diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 802c003f..d17590ef 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -33,3 +33,8 @@ updates: minor-updates: update-types: - "minor" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index bc581582..c21b8985 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -12,13 +12,13 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Log in to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} @@ -28,7 +28,7 @@ jobs: run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - name: Install Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: 1.23.0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d2612cf5..53162f5e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,6 +23,9 @@ jobs: - name: Install dependencies run: npm ci + - name: Create database index.ts + run: echo 'export * from "./sqlite";' > server/db/index.ts + - name: Generate database migrations run: npm run db:sqlite:generate @@ -45,5 +48,8 @@ jobs: echo "App failed to start" exit 1 - - name: Build Docker image + - name: Build Docker image sqlite run: make build + + - name: Build Docker image pg + run: make build-pg diff --git a/Makefile b/Makefile index c2c01b9a..f074c380 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build build-release build-arm build-x86 test clean +.PHONY: build build-pg build-release build-arm build-x86 test clean build-release: @if [ -z "$(tag)" ]; then \ @@ -17,7 +17,10 @@ build-x86: docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest . build: - docker build -t fosrl/pangolin:latest . + docker build -t fosrl/pangolin:latest -f Dockerfile . + +build-pg: + docker build -t fosrl/pangolin:postgresql-latest -f Dockerfile.pg . test: docker run -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -v ./config:/app/config fosrl/pangolin:latest diff --git a/messages/en-US.json b/messages/en-US.json index a0b570fc..ef59a255 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -12,7 +12,7 @@ "componentsErrorNoMember": "You are not currently a member of any organizations.", "welcome": "Welcome to Pangolin", "componentsCreateOrg": "Create an Organization", - "componentsMember": "You're a member of {count, plural, =0 {no organization} =1 {one organization} other {# organizations}}.", + "componentsMember": "You're a member of {count, plural, =0 {no organization} one {one organization} other {# organizations}}.", "componentsInvalidKey": "Invalid or expired license keys detected. Follow license terms to continue using all features.", "dismiss": "Dismiss", "componentsLicenseViolation": "License Violation: This server is using {usedSites} sites which exceeds its licensed limit of {maxSites} sites. Follow license terms to continue using all features.", @@ -249,7 +249,7 @@ "weeks": "Weeks", "months": "Months", "years": "Years", - "day": "{count, plural, =1 {# day} other {# days}}", + "day": "{count, plural, one {# day} other {# days}}", "apiKeysTitle": "API Key Information", "apiKeysConfirmCopy2": "You must confirm that you have copied the API key.", "apiKeysErrorCreate": "Error creating API key", @@ -347,7 +347,7 @@ "licensePurchase": "Purchase License", "licensePurchaseSites": "Purchase Additional Sites", "licenseSitesUsedMax": "{usedSites} of {maxSites} sites used", - "licenseSitesUsed": "{count, plural, =0 {# sites} =1 {# site} other {# sites}} in system.", + "licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}} in system.", "licensePurchaseDescription": "Choose how many sites you want to {selectedMode, select, license {purchase a license for. You can always add more sites later.} other {add to your existing license.}}", "licenseFee": "License fee", "licensePriceSite": "Price per site", @@ -436,7 +436,7 @@ "accessRoleSelect": "Select role", "inviteEmailSentDescription": "An email has been sent to the user with the access link below. They must access the link to accept the invitation.", "inviteSentDescription": "The user has been invited. They must access the link below to accept the invitation.", - "inviteExpiresIn": "The invite will expire in {days, plural, =1 {# day} other {# days}}.", + "inviteExpiresIn": "The invite will expire in {days, plural, one {# day} other {# days}}.", "idpTitle": "Identity Provider", "idpSelect": "Select the identity provider for the external user", "idpNotConfigured": "No identity providers are configured. Please configure an identity provider before creating external users.", @@ -1102,7 +1102,7 @@ "containerNetworks": "Networks", "containerHostnameIp": "Hostname/IP", "containerLabels": "Labels", - "containerLabelsCount": "{count} label{s,plural,one{} other{s}}", + "containerLabelsCount": "{count, plural, one {# label} other {# labels}}", "containerLabelsTitle": "Container Labels", "containerLabelEmpty": "", "containerPorts": "Ports", @@ -1114,7 +1114,7 @@ "showStoppedContainers": "Show stopped containers", "noContainersFound": "No containers found. Make sure Docker containers are running.", "searchContainersPlaceholder": "Search across {count} containers...", - "searchResultsCount": "{count} result{s,plural,one{} other{s}}", + "searchResultsCount": "{count, plural, one {# result} other {# results}}", "filters": "Filters", "filterOptions": "Filter Options", "filterPorts": "Ports", diff --git a/messages/ko-KR.json b/messages/ko-KR.json new file mode 100644 index 00000000..62b1dfc1 --- /dev/null +++ b/messages/ko-KR.json @@ -0,0 +1,1136 @@ +{ + "setupCreate": "조직, 사이트 및 리소스를 생성하십시오.", + "orgDisplayName": "이것은 귀하의 조직의 표시 이름입니다.", + "setupIdentifierMessage": "이것은 귀하의 조직에 대한 고유 식별자입니다. 표시 이름과는 별개입니다.", + "componentsErrorNoMemberCreate": "현재 어떤 조직의 구성원도 아닙니다. 시작하려면 조직을 생성하세요.", + "componentsInvalidKey": "유효하지 않거나 만료된 라이센스 키가 감지되었습니다. 모든 기능을 계속 사용하려면 라이센스 조건을 따르십시오.", + "orgId": "조직 ID", + "siteQuestionRemove": "조직에서 사이트 {selectedSite}를 제거하시겠습니까?", + "siteCreateDescription2": "아래 단계를 따라 새 사이트를 생성하고 연결하십시오", + "componentsLicenseViolation": "라이센스 위반: 이 서버는 {usedSites} 사이트를 사용하고 있으며, 이는 {maxSites} 사이트의 라이센스 한도를 초과합니다. 모든 기능을 계속 사용하려면 라이센스 조건을 따르십시오.", + "years": "연도", + "hours": "시간", + "days": "일", + "weeks": "주", + "months": "개월", + "authCreateAccount": "시작하려면 계정을 생성하세요.", + "email": "이메일", + "password": "비밀번호", + "confirmPassword": "비밀번호 확인", + "createAccount": "계정 생성", + "viewSettings": "설정 보기", + "delete": "삭제", + "name": "이름", + "online": "온라인", + "offline": "오프라인", + "site": "사이트", + "dataIn": "데이터 입력", + "dataOut": "데이터 출력", + "connectionType": "연결 유형", + "tunnelType": "터널 유형", + "local": "로컬", + "edit": "편집", + "siteConfirmDelete": "사이트 삭제 확인", + "siteDelete": "사이트 삭제", + "siteMessageRemove": "제거되면 사이트에 더 이상 접근할 수 없습니다. 사이트와 관련된 모든 리소스와 대상도 제거됩니다.", + "siteMessageConfirm": "확인을 위해 아래에 사이트 이름을 입력해 주세요.", + "setupNewOrg": "새 조직", + "setupCreateOrg": "조직 생성", + "setupCreateResources": "리소스 생성", + "setupOrgName": "조직 이름", + "setupErrorIdentifier": "조직 ID가 이미 사용 중입니다. 다른 것을 선택해 주세요.", + "componentsErrorNoMember": "현재 어떤 조직의 구성원도 아닙니다.", + "welcome": "판골린에 오신 것을 환영합니다.", + "componentsCreateOrg": "조직 생성", + "componentsMember": "당신은 {count, plural, =0 {조직이 없습니다} one {하나의 조직} other {# 개의 조직}}의 구성원입니다.", + "componentsSupporterMessage": "{tier}로 판골린을 지원해 주셔서 감사합니다!", + "inviteErrorNotValid": "죄송하지만, 접근하려는 초대가 수락되지 않았거나 더 이상 유효하지 않은 것 같습니다.", + "inviteErrorUser": "죄송하지만, 접근하려는 초대가 이 사용자에게 해당되지 않는 것 같습니다.", + "inviteLoginUser": "올바른 사용자로 로그인했는지 확인하십시오.", + "inviteErrorNoUser": "죄송하지만, 접근하려는 초대가 존재하지 않는 사용자에 대한 것인 것 같습니다.", + "inviteCreateUser": "먼저 계정을 생성해 주세요.", + "goHome": "홈으로 가기", + "inviteLogInOtherUser": "다른 사용자로 로그인", + "createAnAccount": "계정 만들기", + "siteManageSites": "사이트 관리", + "siteDescription": "안전한 터널을 통해 네트워크에 연결할 수 있도록 허용", + "siteCreate": "사이트 생성", + "inviteNotAccepted": "초대가 수락되지 않음", + "authNoAccount": "계정이 없으신가요?", + "siteCreateDescription": "리소스를 연결하기 위해 새 사이트를 생성하십시오.", + "dismiss": "해제", + "close": "닫기", + "siteErrorCreate": "사이트 생성 오류", + "siteErrorCreateKeyPair": "키 쌍 또는 사이트 기본값을 찾을 수 없습니다", + "siteErrorCreateDefaults": "사이트 기본값을 찾을 수 없습니다", + "siteNameDescription": "이것은 사이트의 표시 이름입니다.", + "method": "방법", + "siteMethodDescription": "이것이 연결을 노출하는 방법입니다.", + "siteLearnNewt": "시스템에 Newt 설치하는 방법 배우기", + "siteSeeConfigOnce": "구성을 한 번만 볼 수 있습니다.", + "siteLoadWGConfig": "WireGuard 구성 로딩 중...", + "siteDocker": "Docker 배포 세부정보 확장", + "toggle": "전환", + "dockerCompose": "도커 컴포즈", + "dockerRun": "도커 실행", + "siteLearnLocal": "로컬 사이트는 터널링하지 않습니다. 자세히 알아보기", + "siteConfirmCopy": "구성을 복사했습니다.", + "searchSitesProgress": "사이트 검색...", + "siteAdd": "사이트 추가", + "siteInstallNewt": "Newt 설치", + "siteInstallNewtDescription": "시스템에서 Newt 실행하기", + "WgConfiguration": "WireGuard 구성", + "WgConfigurationDescription": "네트워크에 연결하기 위한 다음 구성을 사용하십시오.", + "operatingSystem": "운영 체제", + "commands": "명령", + "recommended": "추천", + "siteNewtDescription": "최고의 사용자 경험을 위해 Newt를 사용하십시오. Newt는 WireGuard를 기반으로 하며, 판골린 대시보드 내에서 개인 네트워크의 LAN 주소로 개인 리소스에 접근할 수 있도록 합니다.", + "siteRunsInDocker": "Docker에서 실행", + "siteRunsInShell": "macOS, Linux 및 Windows에서 셸에서 실행", + "siteErrorDelete": "사이트 삭제 오류", + "siteErrorUpdate": "사이트 업데이트에 실패했습니다", + "siteErrorUpdateDescription": "사이트 업데이트 중 오류가 발생했습니다.", + "siteUpdated": "사이트가 업데이트되었습니다", + "siteUpdatedDescription": "사이트가 업데이트되었습니다.", + "siteGeneralDescription": "이 사이트에 대한 일반 설정을 구성하세요.", + "siteSettingDescription": "사이트에서 설정을 구성하세요", + "siteSetting": "{siteName} 설정", + "siteNewtTunnel": "뉴트 터널 (추천)", + "siteNewtTunnelDescription": "네트워크에 대한 진입점을 생성하는 가장 쉬운 방법입니다. 추가 설정이 필요 없습니다.", + "siteWg": "기본 WireGuard", + "siteWgDescription": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다.", + "siteLocalDescription": "로컬 리소스만 사용 가능합니다. 터널링이 없습니다.", + "siteSeeAll": "모든 사이트 보기", + "siteTunnelDescription": "사이트에 연결하는 방법을 결정하세요", + "siteNewtCredentials": "Newt 자격 증명", + "siteNewtCredentialsDescription": "이것이 Newt가 서버와 인증하는 방법입니다", + "orgPolicyDeletedDescription": "정책이 성공적으로 삭제되었습니다", + "actionCreateResourceRule": "리소스 규칙 생성", + "defaultMappingsUpdatedDescription": "기본 매핑이 성공적으로 업데이트되었습니다.", + "orgPoliciesAbout": "조직 정책에 대하여", + "orgPoliciesAboutDescription": "조직 정책은 사용자의 ID 토큰에 따라 조직에 대한 액세스를 제어하는 데 사용됩니다. ID 토큰에서 역할 및 조직 정보를 추출하기 위해 JMESPath 표현식을 지정할 수 있습니다.", + "orgPoliciesAboutDescriptionLink": "자세한 내용은 문서를 참조하십시오.", + "actionDeleteResourceRule": "리소스 규칙 삭제", + "defaultMappingsOptional": "기본 매핑(선택 사항)", + "signupError": "가입하는 동안 오류가 발생했습니다.", + "siteCredentialsSave": "자격 증명 저장", + "siteCredentialsSaveDescription": "이것은 한 번만 볼 수 있습니다. 안전한 장소에 복사해 두세요.", + "siteInfo": "사이트 정보", + "status": "상태", + "shareTitle": "공유 링크 관리", + "shareDescription": "공유 가능한 링크를 생성하여 리소스에 대한 임시 또는 영구 액세스를 부여합니다.", + "shareSearch": "공유 링크 검색...", + "shareCreate": "공유 링크 생성", + "shareErrorDelete": "링크 삭제에 실패했습니다.", + "shareErrorDeleteMessage": "링크 삭제 중 오류가 발생했습니다.", + "shareDeleted": "링크가 삭제되었습니다.", + "shareDeletedDescription": "링크가 삭제되었습니다.", + "shareTokenDescription": "액세스 토큰은 쿼리 매개변수 또는 요청 헤더의 두 가지 방법으로 전달될 수 있습니다. 이는 인증된 액세스를 위해 클라이언트에서 모든 요청마다 전달되어야 합니다.", + "accessToken": "액세스 토큰", + "usageExamples": "사용 예", + "tokenId": "토큰 ID", + "requestHeades": "요청 헤더", + "queryParameter": "쿼리 매개변수", + "importantNote": "중요한 참고 사항", + "shareImportantDescription": "보안상의 이유로 가능한 경우 쿼리 매개변수보다 헤더를 사용하는 것이 권장됩니다. 쿼리 매개변수는 서버 로그나 브라우저 기록에 기록될 수 있습니다.", + "token": "토큰", + "shareTokenSecurety": "액세스 토큰을 안전하게 유지하세요. 공개적으로 접근 가능한 영역이나 클라이언트 측 코드에서 공유하지 마세요.", + "shareErrorFetchResource": "리소스를 가져오는 데 실패했습니다.", + "shareErrorFetchResourceDescription": "리소스를 가져오는 중 오류가 발생했습니다.", + "shareErrorCreate": "공유 링크 생성에 실패했습니다.", + "shareErrorCreateDescription": "공유 링크를 생성하는 동안 오류가 발생했습니다", + "shareCreateDescription": "이 링크가 있는 누구나 리소스에 접근할 수 있습니다.", + "shareTitleOptional": "제목 (선택 사항)", + "expireIn": "만료됨", + "neverExpire": "만료되지 않음", + "shareExpireDescription": "만료 시간은 링크가 사용 가능하고 리소스에 접근할 수 있는 기간입니다. 이 시간이 지나면 링크는 더 이상 작동하지 않으며, 이 링크를 사용한 사용자는 리소스에 대한 접근 권한을 잃게 됩니다.", + "pangolinLogoAlt": "판골린 로고", + "shareSeeOnce": "이 링크는 한 번만 볼 수 있습니다. 반드시 복사해 두세요.", + "shareAccessHint": "이 링크가 있는 누구나 리소스에 접근할 수 있습니다. 주의해서 공유하세요.", + "shareTokenUsage": "액세스 토큰 사용 보기", + "createLink": "링크 생성", + "resourcesNotFound": "리소스가 발견되지 않았습니다.", + "resourceSearch": "리소스 검색", + "openMenu": "메뉴 열기", + "resource": "리소스", + "title": "제목", + "created": "생성됨", + "expires": "만료", + "never": "절대", + "shareErrorSelectResource": "리소스를 선택하세요", + "resourceTitle": "리소스 관리", + "resourceDescription": "개인 애플리케이션에 대한 보안 프록시 생성", + "resourcesSearch": "리소스 검색...", + "resourceAdd": "리소스 추가", + "resourceErrorDelte": "리소스 삭제 중 오류 발생", + "authentication": "인증", + "protected": "보호됨", + "notProtected": "보호되지 않음", + "inviteAlready": "초대받은 것 같습니다!", + "resourceMessageRemove": "제거되면 리소스에 더 이상 접근할 수 없습니다. 리소스와 연결된 모든 대상도 제거됩니다.", + "resourceMessageConfirm": "확인을 위해 아래에 리소스의 이름을 입력하세요.", + "tagsEnteredDescription": "입력한 태그는 다음과 같습니다.", + "resourceQuestionRemove": "조직에서 리소스 {selectedResource}를 제거하시겠습니까?", + "resourceHTTP": "HTTPS 리소스", + "resourceHTTPDescription": "서브도메인 또는 기본 도메인을 사용하여 HTTPS를 통해 앱에 대한 요청을 프록시합니다.", + "resourceRaw": "원시 TCP/UDP 리소스", + "resourceRawDescription": "TCP/UDP를 통해 포트 번호를 사용하여 앱에 요청을 프록시합니다.", + "resourceCreate": "리소스 생성", + "resourceCreateDescription": "아래 단계를 따라 새 리소스를 생성하세요.", + "resourceSeeAll": "모든 리소스 보기", + "resourceInfo": "리소스 정보", + "resourceNameDescription": "이것은 리소스의 표시 이름입니다.", + "siteSelect": "사이트 선택", + "siteSearch": "사이트 검색", + "siteNotFound": "사이트를 찾을 수 없습니다.", + "otpEnable": "이중 인증 활성화", + "siteSelectionDescription": "이 사이트는 리소스에 대한 연결을 제공합니다.", + "resourceType": "리소스 유형", + "resourceTypeDescription": "리소스에 접근하는 방법을 결정하세요", + "resourceHTTPSSettings": "HTTPS 설정", + "resourceHTTPSSettingsDescription": "리소스에 대한 HTTPS 접근 방식을 구성하십시오.", + "domainType": "도메인 유형", + "subdomain": "서브도메인", + "baseDomain": "기본 도메인", + "subdomnainDescription": "리소스에 접근할 수 있는 하위 도메인입니다.", + "resourceRawSettings": "TCP/UDP 설정", + "otpDisable": "이중 인증 비활성화", + "resourceRawSettingsDescription": "TCP/UDP를 통해 리소스에 접근하는 방법을 구성하세요.", + "protocol": "프로토콜", + "protocolSelect": "프로토콜 선택", + "resourcePortNumber": "포트 번호", + "logout": "로그 아웃", + "resourcePortNumberDescription": "요청을 프록시하기 위한 외부 포트 번호입니다.", + "cancel": "취소", + "resourceConfig": "구성 스니펫", + "inviteAlreadyDescription": "초대를 수락하려면 로그인하거나 계정을 생성해야 합니다.", + "resourceConfigDescription": "TCP/UDP 리소스를 설정하기 위해 이 구성 스니펫을 복사하여 붙여넣으십시오.", + "resourceAddEntrypoints": "Traefik: 엔트리포인트 추가", + "resourceExposePorts": "Gerbil: Docker Compose에서 포트 노출", + "resourceLearnRaw": "TCP/UDP 리소스 구성 방법 알아보기", + "resourceBack": "리소스로 돌아가기", + "resourceGoTo": "리소스로 이동", + "resourceDelete": "리소스 삭제", + "resourceDeleteConfirm": "리소스 삭제 확인", + "visibility": "가시성", + "enabled": "활성화됨", + "disabled": "비활성화됨", + "general": "일반", + "generalSettings": "일반 설정", + "proxy": "프록시", + "rules": "규칙", + "resourceSettingDescription": "리소스의 설정을 구성하세요.", + "sidebarApiKeys": "API 키", + "resourceSetting": "{resourceName} 설정", + "alwaysAllow": "항상 허용", + "alwaysDeny": "항상 거부", + "orgSettingsDescription": "조직의 일반 설정을 구성하세요", + "orgGeneralSettings": "조직 설정", + "orgGeneralSettingsDescription": "조직 세부정보 및 구성을 관리하세요.", + "saveGeneralSettings": "일반 설정 저장", + "orgDangerZone": "위험 구역", + "orgDangerZoneDescription": "이 조직을 삭제하면 되돌릴 수 없습니다. 확실히 하세요.", + "orgDelete": "조직 삭제", + "orgDeleteConfirm": "조직 삭제 확인", + "orgMessageRemove": "이 작업은 되돌릴 수 없으며 모든 관련 데이터를 삭제합니다.", + "orgMessageConfirm": "확인을 위해 아래에 조직 이름을 입력하십시오.", + "orgQuestionRemove": "조직 {selectedOrg}을(를) 제거하시겠습니까?", + "orgUpdated": "조직이 업데이트되었습니다.", + "orgUpdatedDescription": "조직이 업데이트되었습니다.", + "orgErrorUpdate": "조직 업데이트에 실패했습니다.", + "orgErrorUpdateMessage": "조직을 업데이트하는 동안 오류가 발생했습니다.", + "sidebarSettings": "설정", + "orgErrorFetch": "조직을 가져오는 데 실패했습니다.", + "orgErrorFetchMessage": "조직을 나열하는 동안 오류가 발생했습니다", + "orgErrorDelete": "조직 삭제에 실패했습니다.", + "orgErrorDeleteMessage": "조직을 삭제하는 중 오류가 발생했습니다.", + "orgDeleted": "조직이 삭제되었습니다.", + "orgDeletedMessage": "조직과 그 데이터가 삭제되었습니다.", + "orgMissing": "조직 ID가 누락되었습니다", + "orgMissingMessage": "조직 ID 없이 초대장을 재생성할 수 없습니다.", + "accessUsersManage": "사용자 관리", + "accessUsersDescription": "사용자를 초대하고 역할에 추가하여 조직에 대한 접근을 관리하세요", + "accessUsersSearch": "사용자 검색...", + "accessUserCreate": "사용자 생성", + "accessUserRemove": "사용자 제거", + "username": "사용자 이름", + "identityProvider": "아이덴티티 공급자", + "role": "역할", + "nameRequired": "이름은 필수입니다", + "accessRolesManage": "역할 관리", + "accessRolesDescription": "조직에 대한 액세스를 관리할 역할 구성", + "accessRolesSearch": "역할 검색...", + "accessRolesAdd": "역할 추가", + "accessRoleDelete": "역할 삭제", + "description": "설명", + "inviteTitle": "열린 초대", + "inviteDescription": "다른 사용자에 대한 초대를 관리하세요", + "inviteSearch": "초대 검색...", + "minutes": "분", + "day": "{count, plural, one {#일} other {#일}}", + "apiKeysTitle": "API 키 정보", + "signupQuestion": "이미 계정이 있습니까?", + "apiKeysConfirmCopy2": "API 키를 복사했음을 확인해야 합니다.", + "apiKeysErrorCreate": "API 키 생성 오류", + "apiKeysErrorSetPermission": "권한 설정 오류", + "apiKeysCreate": "API 키 생성", + "apiKeysCreateDescription": "조직을 위한 새로운 API 키 생성", + "apiKeysGeneralSettings": "권한", + "apiKeysGeneralSettingsDescription": "이 API 키가 수행할 수 있는 작업 결정", + "apiKeysList": "귀하의 API 키", + "apiKeysSave": "API 키 저장", + "apiKeysSaveDescription": "이것은 한 번만 볼 수 있습니다. 안전한 장소에 복사해 두세요.", + "apiKeysInfo": "귀하의 API 키는 다음과 같습니다:", + "apiKeysConfirmCopy": "API 키를 복사했습니다", + "generate": "생성", + "done": "완료", + "apiKeysSeeAll": "모든 API 키 보기", + "apiKeysPermissionsErrorLoadingActions": "API 키 작업 로드 오류", + "apiKeysPermissionsErrorUpdate": "권한 설정 오류", + "apiKeysPermissionsUpdated": "권한이 업데이트되었습니다", + "login": "로그인", + "apiKeysPermissionsUpdatedDescription": "권한이 업데이트되었습니다.", + "apiKeysPermissionsGeneralSettings": "권한", + "apiKeysPermissionsGeneralSettingsDescription": "이 API 키가 수행할 수 있는 작업 결정", + "apiKeysPermissionsSave": "권한 저장", + "apiKeysPermissionsTitle": "권한", + "apiKeys": "API 키", + "searchApiKeys": "API 키 검색...", + "apiKeysAdd": "API 키 생성", + "apiKeysErrorDelete": "API 키 삭제 오류", + "apiKeysErrorDeleteMessage": "API 키 삭제 오류", + "apiKeysQuestionRemove": "조직에서 API 키 {selectedApiKey}를 제거하시겠습니까?", + "apiKeysMessageRemove": "삭제되면 API 키를 더 이상 사용할 수 없습니다.", + "apiKeysMessageConfirm": "확인을 위해 아래에 API 키의 이름을 입력해 주세요.", + "apiKeysDeleteConfirm": "API 키 삭제 확인", + "apiKeysDelete": "API 키 삭제", + "apiKeysManage": "API 키 관리", + "apiKeysDescription": "API 키는 통합 API와 인증하는 데 사용됩니다.", + "apiKeysSettings": "{apiKeyName} 설정", + "userTitle": "모든 사용자 관리", + "userDescription": "시스템의 모든 사용자를 보고 관리합니다", + "userAbount": "사용자 관리에 대한 정보", + "userAbountDescription": "이 표는 시스템의 모든 루트 사용자 객체를 표시합니다. 각 사용자는 여러 조직에 속할 수 있습니다. 사용자를 조직에서 제거해도 루트 사용자 객체는 삭제되지 않으며, 시스템에 남아 있습니다. 사용자를 시스템에서 완전히 제거하려면 이 표의 삭제 작업을 사용하여 루트 사용자 객체를 삭제해야 합니다.", + "userServer": "서버 사용자", + "userSearch": "서버 사용자 검색 중...", + "userErrorDelete": "사용자 삭제 오류", + "userDeleteConfirm": "사용자 삭제 확인", + "userDeleteServer": "서버에서 사용자 삭제", + "userMessageRemove": "사용자가 모든 조직에서 제거되며 서버에서 완전히 삭제됩니다.", + "userMessageConfirm": "확인을 위해 아래에 사용자 이름을 입력하십시오.", + "userQuestionRemove": "정말로 {selectedUser}를 서버에서 영구적으로 삭제하시겠습니까?", + "licenseKey": "라이센스 키", + "valid": "유효", + "numberOfSites": "사이트 수", + "licenseKeySearch": "라이센스 키 검색 중...", + "licenseKeyAdd": "라이센스 키 추가", + "type": "유형", + "licenseKeyRequired": "라이센스 키가 필요합니다", + "licenseTermsAgree": "라이선스 조건에 동의해야 합니다.", + "licenseErrorKeyLoad": "라이센스 키를 로드하는 데 실패했습니다.", + "licenseErrorKeyLoadDescription": "라이센스 키 로드 중 오류가 발생했습니다.", + "licenseErrorKeyDelete": "라이센스 키 삭제에 실패했습니다.", + "resourceNotFound": "리소스를 찾을 수 없습니다", + "licenseErrorKeyDeleteDescription": "라이센스 키 삭제 중 오류가 발생했습니다.", + "licenseKeyDeleted": "라이센스 키가 삭제되었습니다.", + "licenseKeyDeletedDescription": "라이센스 키가 삭제되었습니다.", + "licenseErrorKeyActivate": "라이센스 키 활성화에 실패했습니다.", + "licenseErrorKeyActivateDescription": "라이센스 키를 활성화하는 동안 오류가 발생했습니다", + "licenseAbout": "라이센스에 대한 정보", + "communityEdition": "커뮤니티 에디션", + "licenseAboutDescription": "이것은 상업적 환경에서 Pangolin을 사용하는 비즈니스 및 기업 사용자용입니다. 개인 용도로 Pangolin을 사용하는 경우 이 섹션을 무시할 수 있습니다.", + "licenseKeyActivated": "라이센스 키가 활성화되었습니다", + "licenseKeyActivatedDescription": "라이센스 키가 성공적으로 활성화되었습니다.", + "licenseErrorKeyRecheck": "라이센스 키 재확인 실패", + "licenseErrorKeyRecheckDescription": "라이센스 키를 재확인하는 중 오류가 발생했습니다.", + "licenseErrorKeyRechecked": "라이센스 키가 재확인되었습니다.", + "licenseErrorKeyRecheckedDescription": "모든 라이센스 키가 재검사되었습니다.", + "licenseActivateKey": "라이센스 키 활성화", + "licenseActivateKeyDescription": "라이센스 키를 입력하여 활성화하십시오.", + "licenseActivate": "라이센스 활성화", + "licenseAgreement": "이 상자를 체크함으로써, 귀하는 귀하의 라이선스 키와 관련된 계층에 해당하는 라이선스 조건을 읽고 동의했음을 확인합니다.", + "fossorialLicense": "Fossorial 상업 라이선스 및 구독 약관 보기", + "licenseMessageRemove": "이 작업은 라이센스 키와 그에 의해 부여된 모든 관련 권한을 제거합니다.", + "sidebarAllUsers": "모든 사용자", + "licenseMessageConfirm": "확인을 위해 아래에 라이센스 키를 입력하세요.", + "licenseQuestionRemove": "라이센스 키 {selectedKey}를 삭제하시겠습니까?", + "licenseKeyDelete": "라이센스 키 삭제", + "licenseKeyDeleteConfirm": "라이센스 키 삭제 확인", + "licenseTitle": "라이선스 상태 관리", + "licenseTitleDescription": "시스템에서 라이센스 키를 보고 관리합니다.", + "licenseHost": "호스트 라이센스", + "licenseHostDescription": "호스트의 주요 라이센스 키를 관리합니다.", + "licensedNot": "라이센스 없음", + "hostId": "호스트 ID", + "licenseReckeckAll": "모든 키 재확인", + "licenseSiteUsage": "사이트 사용량", + "licenseSiteUsageDecsription": "이 라이센스를 사용하는 사이트 수를 확인하세요.", + "noResults": "결과를 찾을 수 없습니다.", + "licenseNoSiteLimit": "라이선스가 없는 호스트를 사용하는 사이트 수에 제한이 없습니다.", + "licensePurchase": "라이센스 구매", + "licensePurchaseSites": "추가 사이트 구매", + "licenseSitesUsedMax": "{maxSites}개의 사이트 중 {usedSites}개 사용 중", + "licenseSitesUsed": "시스템에 {count, plural, =0 {# 사이트} one {# 사이트} other {# 사이트}}가 있습니다.", + "licensePurchaseDescription": "구매할 사이트 수를 선택하세요 {selectedMode, select, license {라이센스를 구매합니다. 나중에 더 많은 사이트를 추가할 수 있습니다.} other {기존 라이센스에 추가합니다.}}", + "licenseFee": "라이선스 요금", + "licensePriceSite": "사이트당 가격", + "total": "총계", + "licenseContinuePayment": "결제로 진행", + "pricingPage": "가격 페이지", + "pricingPortal": "구매 포털 보기", + "licensePricingPage": "가장 최신의 가격 및 할인 정보를 보려면 방문하십시오 ", + "invite": "초대", + "inviteRegenerate": "초대장 재생성", + "inviteRegenerateDescription": "이전 초대를 취소하고 새로 생성", + "inviteRemove": "초대 제거", + "inviteRemoveError": "초대 제거 실패", + "inviteRemoveErrorDescription": "초대를 제거하는 동안 오류가 발생했습니다.", + "inviteRemoved": "초대가 제거되었습니다.", + "inviteRemovedDescription": "{email}에 대한 초대가 삭제되었습니다.", + "inviteQuestionRemove": "초대 {email}를 제거하시겠습니까?", + "inviteMessageRemove": "한 번 제거되면 이 초대는 더 이상 유효하지 않습니다. 나중에 사용자를 다시 초대할 수 있습니다.", + "inviteMessageConfirm": "확인을 위해 아래 초대의 이메일 주소를 입력해 주세요.", + "inviteQuestionRegenerate": "{email}에 대한 초대장을 다시 생성하시겠습니까? 이전 초대장은 취소됩니다.", + "inviteRemoveConfirm": "초대 제거 확인", + "inviteRegenerated": "초대 재생성됨", + "inviteSent": "새 초대장이 {email}로 전송되었습니다.", + "inviteSentEmail": "사용자에게 이메일 알림 전송", + "inviteGenerate": "{email}에 대한 새로운 초대장이 생성되었습니다.", + "inviteDuplicateError": "초대 중복", + "inviteDuplicateErrorDescription": "이 사용자에 대한 초대가 이미 존재합니다.", + "inviteRateLimitError": "요청 한도 초과", + "inviteRateLimitErrorDescription": "시간당 3회 재생성 한도를 초과했습니다. 나중에 다시 시도하세요.", + "inviteRegenerateError": "초대 재생성 실패", + "inviteRegenerateErrorDescription": "초대장을 재생성하는 동안 오류가 발생했습니다.", + "inviteValidityPeriod": "유효 기간", + "inviteValidityPeriodSelect": "유효 기간 선택", + "inviteRegenerateMessage": "초대장이 다시 생성되었습니다. 사용자는 아래 링크에 접속하여 초대장을 수락해야 합니다.", + "inviteRegenerateButton": "재생성", + "expiresAt": "만료 시간", + "accessRoleUnknown": "알 수 없는 역할", + "placeholder": "자리 표시자", + "userErrorOrgRemove": "사용자를 제거하지 못했습니다", + "userErrorOrgRemoveDescription": "사용자를 제거하는 동안 오류가 발생했습니다.", + "userOrgRemoved": "사용자가 제거되었습니다.", + "userOrgRemovedDescription": "사용자 {email}가 조직에서 제거되었습니다.", + "userQuestionOrgRemove": "{email}을 조직에서 제거하시겠습니까?", + "userMessageOrgRemove": "이 사용자가 제거되면 더 이상 조직에 접근할 수 없습니다. 나중에 다시 초대할 수 있지만, 초대를 다시 수락해야 합니다.", + "userMessageOrgConfirm": "확인을 위해 아래에 사용자 이름을 입력하세요.", + "userRemoveOrgConfirm": "사용자 제거 확인", + "userRemoveOrg": "조직에서 사용자 제거", + "users": "사용자", + "accessRoleMember": "회원", + "accessRoleOwner": "소유자", + "userConfirmed": "확인됨", + "idpNameInternal": "내부", + "emailInvalid": "유효하지 않은 이메일 주소입니다.", + "inviteValidityDuration": "지속 시간을 선택하십시오.", + "accessRoleSelectPlease": "역할을 선택하세요", + "usernameRequired": "사용자 이름은 필수입니다.", + "idpSelectPlease": "신원 제공자를 선택하십시오", + "idpGenericOidc": "일반 OAuth2/OIDC 공급자.", + "accessRoleErrorFetch": "역할을 가져오는 데 실패했습니다.", + "accessRoleErrorFetchDescription": "역할을 가져오는 중 오류가 발생했습니다.", + "idpErrorFetch": "신원 제공자를 가져오는 데 실패했습니다", + "idpErrorFetchDescription": "신원 공급자를 가져오는 중 오류가 발생했습니다.", + "userErrorExists": "사용자가 이미 존재합니다.", + "terabytes": "{count} TB", + "userErrorExistsDescription": "이 사용자는 이미 조직의 구성원입니다.", + "inviteError": "사용자 초대에 실패했습니다", + "inviteErrorDescription": "사용자를 초대하는 동안 오류가 발생했습니다.", + "userInvited": "사용자가 초대되었습니다.", + "userInvitedDescription": "사용자가 성공적으로 초대되었습니다.", + "userErrorCreate": "사용자 생성에 실패했습니다.", + "userErrorCreateDescription": "사용자를 생성하는 동안 오류가 발생했습니다.", + "userCreated": "사용자가 생성되었습니다.", + "userCreatedDescription": "사용자가 성공적으로 생성되었습니다.", + "userTypeInternal": "내부 사용자", + "userTypeInternalDescription": "사용자를 초대하여 귀하의 조직에 직접 참여하게 하세요.", + "userTypeExternal": "외부 사용자", + "userTypeExternalDescription": "외부 신원 공급자를 사용하여 사용자를 생성하세요.", + "accessUserCreateDescription": "새 사용자를 만들기 위한 아래 단계를 따르세요.", + "userSeeAll": "모든 사용자 보기", + "userTypeTitle": "사용자 유형", + "userTypeDescription": "사용자를 생성하는 방법을 결정하세요.", + "userSettings": "사용자 정보", + "userSettingsDescription": "새 사용자에 대한 세부정보를 입력하십시오.", + "inviteEmailSent": "사용자에게 초대 이메일 보내기", + "inviteValid": "유효 기간", + "selectDuration": "지속 시간 선택", + "accessRoleSelect": "역할 선택", + "inviteEmailSentDescription": "아래의 접근 링크와 함께 사용자에게 이메일이 전송되었습니다. 사용자는 초대를 수락하기 위해 링크에 접근해야 합니다.", + "inviteSentDescription": "사용자가 초대되었습니다. 초대를 수락하려면 아래 링크에 접속해야 합니다.", + "inviteExpiresIn": "초대는 {days, plural, one {#일} other {#일}} 후에 만료됩니다.", + "idpTitle": "아이덴티티 공급자", + "idpSelect": "외부 사용자를 위한 아이덴티티 공급자를 선택하십시오", + "idpNotConfigured": "구성된 아이덴티티 공급자가 없습니다. 외부 사용자를 생성하기 전에 아이덴티티 공급자를 구성하십시오.", + "usernameUniq": "선택한 아이덴티티 공급자에 존재하는 고유한 사용자 이름과 일치해야 합니다.", + "emailOptional": "이메일 (선택 사항)", + "nameOptional": "이름 (선택 사항)", + "accessControls": "접근 제어", + "userDescription2": "이 사용자의 설정 관리", + "accessRoleErrorAdd": "사용자를 역할에 추가하는 데 실패했습니다.", + "accessRoleErrorAddDescription": "사용자를 역할에 추가하는 동안 오류가 발생했습니다.", + "userSaved": "사용자 저장됨", + "userSavedDescription": "사용자가 업데이트되었습니다.", + "accessControlsDescription": "이 사용자가 조직에서 접근하고 수행할 수 있는 작업을 관리하세요", + "accessControlsSubmit": "접근 제어 저장", + "roles": "역할", + "accessUsersRoles": "사용자 및 역할 관리", + "accessUsersRolesDescription": "사용자를 초대하고 역할에 추가하여 조직에 대한 접근을 관리하세요", + "key": "키", + "createdAt": "생성일", + "proxyErrorInvalidHeader": "잘못된 사용자 정의 호스트 헤더 값입니다. 도메인 이름 형식을 사용하거나 사용자 정의 호스트 헤더를 해제하려면 비워 두십시오.", + "proxyErrorTls": "유효하지 않은 TLS 서버 이름입니다. 도메인 이름 형식을 사용하거나 비워 두어 TLS 서버 이름을 제거하십시오.", + "proxyEnableSSL": "SSL 활성화 (https)", + "targetErrorFetch": "대상 가져오는 데 실패했습니다.", + "targetErrorFetchDescription": "대상 가져오는 중 오류가 발생했습니다", + "siteErrorFetch": "리소스를 가져오는 데 실패했습니다", + "siteErrorFetchDescription": "리소스를 가져오는 동안 오류가 발생했습니다", + "targetErrorDuplicate": "중복 대상", + "targetErrorDuplicateDescription": "이 설정을 가진 대상이 이미 존재합니다", + "targetWireGuardErrorInvalidIp": "유효하지 않은 대상 IP", + "targetWireGuardErrorInvalidIpDescription": "대상 IP는 사이트 서브넷 내에 있어야 합니다.", + "targetsUpdated": "대상 업데이트됨", + "targetsUpdatedDescription": "대상 및 설정이 성공적으로 업데이트되었습니다.", + "targetsErrorUpdate": "대상 업데이트 실패", + "targetsErrorUpdateDescription": "대상 업데이트 중 오류가 발생했습니다.", + "targetTlsUpdate": "TLS 설정이 업데이트되었습니다.", + "targetTlsUpdateDescription": "TLS 설정이 성공적으로 업데이트되었습니다.", + "targetErrorTlsUpdate": "TLS 설정 업데이트에 실패했습니다.", + "targetErrorTlsUpdateDescription": "TLS 설정을 업데이트하는 동안 오류가 발생했습니다", + "proxyUpdated": "프록시 설정이 업데이트되었습니다.", + "proxyUpdatedDescription": "프록시 설정이 성공적으로 업데이트되었습니다", + "proxyErrorUpdate": "프록시 설정 업데이트에 실패했습니다.", + "proxyErrorUpdateDescription": "프록시 설정을 업데이트하는 동안 오류가 발생했습니다", + "targetAddr": "IP / 호스트 이름", + "targetPort": "포트", + "targetProtocol": "프로토콜", + "targetTlsSettings": "보안 연결 구성", + "targetTlsSettingsDescription": "리소스에 대한 SSL/TLS 설정 구성", + "targetTlsSettingsAdvanced": "고급 TLS 설정", + "targetTlsSni": "TLS 서버 이름 (SNI)", + "targetTlsSniDescription": "SNI에 사용할 TLS 서버 이름. 기본값을 사용하려면 비워 두십시오.", + "targetTlsSubmit": "설정 저장", + "targets": "대상 구성", + "targetsDescription": "서비스로 트래픽을 라우팅할 대상을 설정하십시오", + "targetStickySessions": "스티키 세션 활성화", + "targetStickySessionsDescription": "세션 전체 동안 동일한 백엔드 대상을 유지합니다.", + "methodSelect": "선택 방법", + "targetSubmit": "대상 추가", + "targetNoOne": "대상이 없습니다. 양식을 사용하여 대상을 추가하세요.", + "targetNoOneDescription": "위에 하나 이상의 대상을 추가하면 로드 밸런싱이 활성화됩니다.", + "targetsSubmit": "대상 저장", + "proxyAdditional": "추가 프록시 설정", + "proxyAdditionalDescription": "리소스가 프록시 설정을 처리하는 방법 구성", + "proxyCustomHeader": "사용자 정의 호스트 헤더", + "proxyCustomHeaderDescription": "요청을 프록시할 때 설정할 호스트 헤더입니다. 기본값을 사용하려면 비워 두십시오.", + "proxyAdditionalSubmit": "프록시 설정 저장", + "subnetMaskErrorInvalid": "유효하지 않은 서브넷 마스크입니다. 0에서 32 사이여야 합니다.", + "ipAddressErrorInvalidFormat": "잘못된 IP 주소 형식", + "ipAddressErrorInvalidOctet": "유효하지 않은 IP 주소 옥텟", + "path": "경로", + "ipAddressRange": "IP 범위", + "rulesErrorFetch": "규칙을 가져오는 데 실패했습니다.", + "rulesErrorFetchDescription": "규칙을 가져오는 중 오류가 발생했습니다", + "rulesErrorDuplicate": "중복 규칙", + "rulesErrorDuplicateDescription": "이 설정을 가진 규칙이 이미 존재합니다.", + "rulesErrorInvalidIpAddressRange": "유효하지 않은 CIDR", + "rulesErrorInvalidIpAddressRangeDescription": "유효한 CIDR 값을 입력하십시오.", + "rulesErrorInvalidUrl": "유효하지 않은 URL 경로", + "rulesErrorInvalidUrlDescription": "유효한 URL 경로 값을 입력해 주세요.", + "rulesErrorInvalidIpAddress": "유효하지 않은 IP", + "rulesErrorInvalidIpAddressDescription": "유효한 IP 주소를 입력하세요", + "rulesErrorUpdate": "규칙 업데이트에 실패했습니다.", + "rulesErrorUpdateDescription": "규칙 업데이트 중 오류가 발생했습니다.", + "rulesUpdated": "규칙 활성화", + "rulesUpdatedDescription": "규칙 평가가 업데이트되었습니다", + "rulesMatchIpAddressRangeDescription": "CIDR 형식으로 주소를 입력하세요 (예: 103.21.244.0/22)", + "rulesMatchIpAddress": "IP 주소를 입력하세요 (예: 103.21.244.12)", + "rulesMatchUrl": "URL 경로 또는 패턴을 입력하세요 (예: /api/v1/todos 또는 /api/v1/*)", + "rulesErrorInvalidPriority": "유효하지 않은 우선순위", + "rulesErrorInvalidPriorityDescription": "유효한 우선 순위를 입력하세요.", + "rulesErrorDuplicatePriority": "중복 우선순위", + "rulesErrorDuplicatePriorityDescription": "고유한 우선 순위를 입력하십시오.", + "ruleUpdated": "규칙이 업데이트되었습니다", + "ruleUpdatedDescription": "규칙이 성공적으로 업데이트되었습니다", + "ruleErrorUpdate": "작업 실패", + "ruleErrorUpdateDescription": "저장 작업 중 오류가 발생했습니다.", + "rulesPriority": "우선순위", + "rulesAction": "작업", + "rulesMatchType": "일치 유형", + "value": "값", + "rulesAbout": "규칙에 대한 정보", + "rulesAboutDescription": "규칙을 사용하면 IP 주소 또는 URL 경로를 기준으로 리소스에 대한 액세스를 제어할 수 있습니다. IP 주소 또는 URL 경로를 기준으로 액세스를 허용하거나 거부하는 규칙을 만들 수 있습니다.", + "rulesActions": "작업", + "rulesActionAlwaysAllow": "항상 허용: 모든 인증 방법 우회", + "rulesActionAlwaysDeny": "항상 거부: 모든 요청을 차단합니다. 인증을 시도할 수 없습니다.", + "rulesMatchCriteria": "일치 기준", + "rulesMatchCriteriaIpAddress": "특정 IP 주소와 일치", + "rulesMatchCriteriaIpAddressRange": "CIDR 표기법으로 IP 주소 범위를 일치시킵니다", + "rulesMatchCriteriaUrl": "URL 경로 또는 패턴 일치", + "rulesEnable": "규칙 활성화", + "rulesEnableDescription": "이 리소스에 대한 규칙 평가를 활성화하거나 비활성화합니다.", + "rulesResource": "리소스 규칙 구성", + "rulesResourceDescription": "리소스에 대한 접근을 제어하는 규칙 구성", + "ruleSubmit": "규칙 추가", + "rulesNoOne": "규칙이 없습니다. 양식을 사용하여 규칙을 추가하십시오.", + "rulesOrder": "규칙은 우선 순위에 따라 오름차순으로 평가됩니다.", + "rulesSubmit": "규칙 저장", + "resourceErrorCreate": "리소스 생성 오류", + "resourceErrorCreateDescription": "리소스를 생성하는 중 오류가 발생했습니다.", + "resourceErrorCreateMessage": "리소스 생성 오류:", + "resourceErrorCreateMessageDescription": "예기치 않은 오류가 발생했습니다.", + "sitesErrorFetch": "사이트를 가져오는 중 오류가 발생했습니다.", + "sitesErrorFetchDescription": "사이트를 가져오는 중 오류가 발생했습니다", + "domainsErrorFetch": "도메인 가져오기 오류", + "domainsErrorFetchDescription": "도메인을 가져오는 중 오류가 발생했습니다.", + "none": "없음", + "unknown": "알 수 없음", + "resources": "리소스", + "resourcesDescription": "리소스는 개인 네트워크에서 실행 중인 애플리케이션에 대한 프록시입니다. 개인 네트워크에서 HTTP/HTTPS 또는 원시 TCP/UDP 서비스에 대한 리소스를 생성하십시오. 각 리소스는 암호화된 WireGuard 터널을 통해 개인적이고 안전한 연결을 가능하게 하려면 사이트에 연결되어야 합니다.", + "resourcesWireGuardConnect": "WireGuard 암호화를 통한 안전한 연결", + "resourcesMultipleAuthenticationMethods": "다중 인증 방법 구성", + "resourcesUsersRolesAccess": "사용자 및 역할 기반 접근 제어", + "resourcesErrorUpdate": "리소스를 전환하는 데 실패했습니다.", + "resourcesErrorUpdateDescription": "리소스를 업데이트하는 동안 오류가 발생했습니다.", + "access": "접속", + "shareLink": "{resource} 공유 링크", + "resourceSelect": "리소스 선택", + "shareLinks": "공유 링크", + "share": "공유 가능한 링크", + "shareDescription2": "리소스에 대한 공유 가능한 링크를 생성하세요. 링크는 리소스에 대한 임시 또는 무제한 액세스를 제공합니다. 링크를 생성할 때 만료 기간을 설정할 수 있습니다.", + "shareEasyCreate": "생성하고 공유하기 쉬움", + "shareConfigurableExpirationDuration": "구성 가능한 만료 기간", + "shareSecureAndRevocable": "안전하고 철회 가능", + "nameMin": "이름은 최소 {len}자 이상이어야 합니다.", + "nameMax": "이름은 {len}자보다 길 수 없습니다.", + "sitesConfirmCopy": "구성을 복사했는지 확인하십시오.", + "unknownCommand": "알 수 없는 명령", + "newtErrorFetchReleases": "릴리스 정보를 가져오는 데 실패했습니다: {err}", + "newtErrorFetchLatest": "최신 릴리스를 가져오는 중 오류 발생: {err}", + "newtEndpoint": "Newt 엔드포인트", + "newtId": "뉴트 ID", + "newtSecretKey": "Newt 비밀 키", + "architecture": "아키텍처", + "sites": "사이트", + "siteWgAnyClients": "WireGuard 클라이언트를 사용하여 연결하십시오. 피어 IP를 사용하여 내부 리소스에 접근해야 합니다.", + "siteWgCompatibleAllClients": "모든 WireGuard 클라이언트와 호환", + "siteWgManualConfigurationRequired": "수동 구성이 필요합니다.", + "userErrorNotAdminOrOwner": "사용자는 관리자 또는 소유자가 아닙니다.", + "pangolinSettings": "설정 - 판골린", + "accessRoleYour": "귀하의 역할:", + "accessRoleSelect2": "역할 선택", + "accessUserSelect": "사용자를 선택하세요.", + "otpEmailEnter": "이메일을 입력하세요", + "otpEmailEnterDescription": "입력 필드에 입력한 후 Enter 키를 눌러 이메일을 추가합니다.", + "otpEmailErrorInvalid": "유효하지 않은 이메일 주소입니다. 와일드카드(*)는 전체 로컬 부분이어야 합니다.", + "otpEmailSmtpRequired": "SMTP 필요", + "otpEmailSmtpRequiredDescription": "일회성 비밀번호 인증을 사용하려면 서버에서 SMTP가 활성화되어 있어야 합니다.", + "otpEmailTitle": "일회용 비밀번호", + "otpEmailTitleDescription": "리소스 접근을 위한 이메일 기반 인증 필요", + "otpEmailWhitelist": "이메일 화이트리스트", + "otpEmailWhitelistList": "화이트리스트된 이메일", + "otpEmailWhitelistListDescription": "이 이메일 주소를 가진 사용자만 이 리소스에 접근할 수 있습니다. 그들은 이메일로 전송된 일회용 비밀번호를 입력하라는 메시지를 받게 됩니다. 도메인에서 모든 이메일 주소를 허용하기 위해 와일드카드(*@example.com)를 사용할 수 있습니다.", + "otpEmailWhitelistSave": "허용 목록 저장", + "passwordAdd": "비밀번호 추가", + "passwordRemove": "비밀번호 제거", + "pincodeAdd": "PIN 코드 추가", + "pincodeRemove": "PIN 코드 제거", + "resourceAuthMethods": "인증 방법", + "resourceAuthMethodsDescriptions": "추가 인증 방법을 통해 리소스에 대한 액세스 허용", + "resourceAuthSettingsSave": "성공적으로 저장되었습니다.", + "resourceAuthSettingsSaveDescription": "인증 설정이 저장되었습니다", + "resourceErrorAuthFetch": "데이터를 가져오는 데 실패했습니다.", + "resourceErrorAuthFetchDescription": "데이터를 가져오는 중 오류가 발생했습니다.", + "resourceErrorPasswordRemove": "리소스 비밀번호 제거 오류", + "resourceErrorPasswordRemoveDescription": "리소스 비밀번호를 제거하는 동안 오류가 발생했습니다.", + "resourceErrorPasswordSetup": "리소스 비밀번호 설정 오류", + "resourceErrorPasswordSetupDescription": "리소스 비밀번호 설정 중 오류가 발생했습니다", + "resourceErrorPincodeRemove": "리소스 핀 코드 제거 오류", + "resourceErrorPincodeRemoveDescription": "리소스 핀코드를 제거하는 중 오류가 발생했습니다.", + "resourceErrorPincodeSetup": "리소스 PIN 코드 설정 중 오류가 발생했습니다.", + "resourceErrorPincodeSetupDescription": "리소스 PIN 코드를 설정하는 동안 오류가 발생했습니다.", + "resourceErrorUsersRolesSave": "역할 설정에 실패했습니다.", + "resourceErrorUsersRolesSaveDescription": "역할 설정 중 오류가 발생했습니다.", + "resourceErrorWhitelistSave": "화이트리스트 저장에 실패했습니다.", + "resourceErrorWhitelistSaveDescription": "화이트리스트를 저장하는 동안 오류가 발생했습니다.", + "resourcePasswordSubmit": "비밀번호 보호 활성화", + "resourcePasswordProtection": "비밀번호 보호 {status}", + "resourcePasswordRemove": "리소스 비밀번호가 제거되었습니다", + "resourcePasswordRemoveDescription": "리소스 비밀번호가 성공적으로 제거되었습니다.", + "resourcePasswordSetup": "리소스 비밀번호 설정됨", + "resourcePasswordSetupDescription": "리소스 비밀번호가 성공적으로 설정되었습니다.", + "resourcePasswordSetupTitle": "비밀번호 설정", + "resourcePasswordSetupTitleDescription": "이 리소스를 보호하기 위해 비밀번호를 설정하세요.", + "resourcePincode": "PIN 코드", + "resourcePincodeSubmit": "PIN 코드 보호 활성화", + "resourcePincodeProtection": "PIN 코드 보호 {상태}", + "resourcePincodeRemove": "리소스 핀코드가 제거되었습니다.", + "resourcePincodeRemoveDescription": "리소스 비밀번호가 성공적으로 제거되었습니다.", + "resourcePincodeSetup": "리소스 PIN 코드가 설정되었습니다", + "resourcePincodeSetupDescription": "리소스 핀코드가 성공적으로 설정되었습니다", + "resourcePincodeSetupTitle": "핀코드 설정", + "resourcePincodeSetupTitleDescription": "이 리소스를 보호하기 위해 핀 코드를 설정하십시오.", + "resourceRoleDescription": "관리자는 항상 이 리소스에 접근할 수 있습니다.", + "resourceUsersRoles": "사용자 및 역할", + "resourceUsersRolesDescription": "이 리소스를 방문할 수 있는 사용자 및 역할을 구성하십시오", + "resourceUsersRolesSubmit": "사용자 및 역할 저장", + "resourceWhitelistSave": "성공적으로 저장되었습니다.", + "resourceWhitelistSaveDescription": "허용 목록 설정이 저장되었습니다.", + "ssoUse": "플랫폼 SSO 사용", + "ssoUseDescription": "기존 사용자는 이 기능이 활성화된 모든 리소스에 대해 한 번만 로그인하면 됩니다.", + "proxyErrorInvalidPort": "유효하지 않은 포트 번호", + "subdomainErrorInvalid": "잘못된 하위 도메인", + "domainErrorFetch": "도메인 가져오기 오류", + "domainErrorFetchDescription": "도메인을 가져오는 중 오류가 발생했습니다.", + "resourceErrorUpdate": "리소스 업데이트에 실패했습니다.", + "resourceErrorUpdateDescription": "리소스를 업데이트하는 동안 오류가 발생했습니다.", + "resourceUpdated": "리소스가 업데이트되었습니다.", + "resourceUpdatedDescription": "리소스가 성공적으로 업데이트되었습니다.", + "resourceErrorTransfer": "리소스 전송에 실패했습니다", + "resourceErrorTransferDescription": "리소스를 전송하는 동안 오류가 발생했습니다", + "resourceTransferred": "리소스가 전송되었습니다.", + "resourceTransferredDescription": "리소스가 성공적으로 전송되었습니다.", + "gigabytes": "{count} GB", + "resourceErrorToggle": "리소스를 전환하는 데 실패했습니다.", + "resourceErrorToggleDescription": "리소스를 업데이트하는 동안 오류가 발생했습니다.", + "resourceVisibilityTitle": "가시성", + "resourceVisibilityTitleDescription": "리소스 가시성을 완전히 활성화하거나 비활성화", + "resourceGeneral": "일반 설정", + "resourceGeneralDescription": "이 리소스에 대한 일반 설정을 구성하십시오.", + "resourceEnable": "리소스 활성화", + "resourceTransfer": "리소스 전송", + "resourceTransferDescription": "이 리소스를 다른 사이트로 전송", + "resourceTransferSubmit": "리소스 전송", + "siteDestination": "대상 사이트", + "searchSites": "사이트 검색", + "accessRoleCreate": "역할 생성", + "accessRoleCreateDescription": "사용자를 그룹화하고 권한을 관리하기 위해 새 역할을 생성하세요.", + "accessRoleCreateSubmit": "역할 생성", + "accessRoleCreated": "역할이 생성되었습니다.", + "accessRoleCreatedDescription": "역할이 성공적으로 생성되었습니다.", + "accessRoleErrorCreate": "역할 생성 실패", + "accessRoleErrorCreateDescription": "역할 생성 중 오류가 발생했습니다.", + "accessRoleErrorNewRequired": "새 역할이 필요합니다.", + "accessRoleErrorRemove": "역할 제거에 실패했습니다.", + "accessRoleErrorRemoveDescription": "역할을 제거하는 동안 오류가 발생했습니다.", + "accessRoleName": "역할 이름", + "accessRoleQuestionRemove": "{name} 역할을 삭제하려고 합니다. 이 작업은 취소할 수 없습니다.", + "accessRoleRemove": "역할 제거", + "accessRoleRemoveDescription": "조직에서 역할 제거", + "accessRoleRemoveSubmit": "역할 제거", + "accessRoleRemoved": "역할이 제거되었습니다", + "accessRoleRemovedDescription": "역할이 성공적으로 제거되었습니다.", + "accessRoleRequiredRemove": "이 역할을 삭제하기 전에 기존 구성원을 전송할 새 역할을 선택하세요.", + "manage": "관리", + "sitesNotFound": "사이트를 찾을 수 없습니다.", + "pangolinServerAdmin": "서버 관리자 - 판골린", + "licenseTierProfessional": "전문 라이센스", + "licenseTierEnterprise": "기업 라이선스", + "licenseTierCommercial": "상업용 라이선스", + "licensed": "라이센스", + "yes": "예", + "no": "아니요", + "sitesAdditional": "추가 사이트", + "licenseKeys": "라이센스 키", + "sitestCountDecrease": "사이트 수 줄이기", + "sitestCountIncrease": "사이트 수 증가", + "idpManage": "아이덴티티 공급자 관리", + "idpManageDescription": "시스템에서 ID 제공자를 보고 관리합니다", + "idpDeletedDescription": "신원 공급자가 성공적으로 삭제되었습니다", + "idpOidc": "OAuth2/OIDC", + "idpQuestionRemove": "정말로 아이덴티티 공급자 {name}를 영구적으로 삭제하시겠습니까?", + "idpMessageRemove": "이 작업은 아이덴티티 공급자와 모든 관련 구성을 제거합니다. 이 공급자를 통해 인증하는 사용자는 더 이상 로그인할 수 없습니다.", + "idpMessageConfirm": "확인을 위해 아래에 아이덴티티 제공자의 이름을 입력하세요.", + "idpConfirmDelete": "신원 제공자 삭제 확인", + "idpDelete": "아이덴티티 공급자 삭제", + "idp": "신원 공급자", + "idpSearch": "ID 공급자 검색...", + "idpAdd": "아이덴티티 공급자 추가", + "idpClientIdRequired": "클라이언트 ID가 필요합니다.", + "idpClientSecretRequired": "클라이언트 비밀이 필요합니다.", + "idpErrorAuthUrlInvalid": "인증 URL은 유효한 URL이어야 합니다.", + "idpErrorTokenUrlInvalid": "토큰 URL은 유효한 URL이어야 합니다.", + "idpPathRequired": "식별자 경로가 필요합니다.", + "idpScopeRequired": "범위가 필요합니다.", + "idpOidcDescription": "OpenID Connect ID 공급자를 구성하십시오.", + "idpCreatedDescription": "ID 공급자가 성공적으로 생성되었습니다.", + "idpCreate": "아이덴티티 공급자 생성", + "idpCreateDescription": "사용자 인증을 위한 새로운 ID 공급자를 구성합니다.", + "idpSeeAll": "모든 ID 공급자 보기", + "idpSettingsDescription": "신원 제공자의 기본 정보를 구성하세요", + "idpDisplayName": "이 신원 공급자를 위한 표시 이름", + "idpAutoProvisionUsers": "사용자 자동 프로비저닝", + "idpAutoProvisionUsersDescription": "활성화되면 사용자가 첫 로그인 시 시스템에 자동으로 생성되며, 사용자와 역할 및 조직을 매핑할 수 있습니다.", + "licenseBadge": "전문가", + "idpType": "제공자 유형", + "idpTypeDescription": "구성할 ID 공급자의 유형을 선택하십시오.", + "idpOidcConfigure": "OAuth2/OIDC 구성", + "idpOidcConfigureDescription": "OAuth2/OIDC 공급자 엔드포인트 및 자격 증명을 구성하십시오.", + "idpClientId": "클라이언트 ID", + "idpClientIdDescription": "아이덴티티 공급자의 OAuth2 클라이언트 ID", + "idpClientSecret": "클라이언트 비밀", + "idpClientSecretDescription": "신원 제공자로부터의 OAuth2 클라이언트 비밀", + "idpAuthUrl": "인증 URL", + "idpAuthUrlDescription": "OAuth2 인증 엔드포인트 URL", + "idpTokenUrl": "토큰 URL", + "idpTokenUrlDescription": "OAuth2 토큰 엔드포인트 URL", + "idpOidcConfigureAlert": "중요 정보", + "idpOidcConfigureAlertDescription": "아이덴티티 공급자를 생성한 후, 아이덴티티 공급자의 설정에서 콜백 URL을 구성해야 합니다. 콜백 URL은 성공적으로 생성된 후 제공됩니다.", + "idpToken": "토큰 구성", + "idpTokenDescription": "ID 토큰에서 사용자 정보를 추출하는 방법 구성", + "idpJmespathAbout": "JMESPath에 대하여", + "idpJmespathAboutDescription": "아래 경로는 ID 토큰에서 값을 추출하기 위해 JMESPath 구문을 사용합니다.", + "idpJmespathAboutDescriptionLink": "JMESPath에 대해 더 알아보기", + "idpJmespathLabel": "식별자 경로", + "idpJmespathLabelDescription": "ID 토큰에서 사용자 식별자에 대한 경로", + "idpJmespathEmailPathOptional": "이메일 경로 (선택 사항)", + "idpJmespathEmailPathOptionalDescription": "ID 토큰에서 사용자의 이메일 경로", + "idpJmespathNamePathOptional": "이름 경로 (선택 사항)", + "idpJmespathNamePathOptionalDescription": "ID 토큰에서 사용자의 이름 경로", + "idpOidcConfigureScopes": "범위", + "idpOidcConfigureScopesDescription": "요청할 OAuth2 범위의 공백으로 구분된 목록", + "idpSubmit": "아이덴티티 공급자 생성", + "orgPolicies": "조직 정책", + "idpSettings": "{idpName} 설정", + "megabytes": "{count} MB", + "actionCheckOrgId": "ID 확인", + "idpCreateSettingsDescription": "아이덴티티 공급자의 설정을 구성하십시오", + "roleMapping": "역할 매핑", + "orgMapping": "조직 매핑", + "orgPoliciesSearch": "조직 정책 검색...", + "orgPoliciesAdd": "조직 정책 추가", + "orgRequired": "조직은 필수입니다.", + "error": "오류", + "success": "성공", + "orgPolicyAddedDescription": "정책이 성공적으로 추가되었습니다", + "orgPolicyUpdatedDescription": "정책이 성공적으로 업데이트되었습니다.", + "tagsEntered": "입력된 태그", + "defaultMappingsOptionalDescription": "조직에 대해 정의된 정책이 없을 때 기본 매핑이 사용됩니다. 여기에서 기본 역할 및 조직 매핑을 지정하여 대체할 수 있습니다.", + "defaultMappingsRole": "기본 역할 매핑", + "defaultMappingsRoleDescription": "이 표현식의 결과는 조직에서 정의된 역할 이름을 문자열로 반환해야 합니다.", + "defaultMappingsOrg": "기본 조직 매핑", + "defaultMappingsOrgDescription": "이 표현식은 사용자가 조직에 접근할 수 있도록 조직 ID 또는 true를 반환해야 합니다.", + "defaultMappingsSubmit": "기본 매핑 저장", + "orgPoliciesEdit": "조직 정책 편집", + "org": "조직", + "orgSelect": "조직 선택", + "orgSearch": "조직 검색", + "orgNotFound": "조직을 찾을 수 없습니다.", + "roleMappingPathOptional": "역할 매핑 경로 (선택 사항)", + "orgMappingPathOptional": "조직 매핑 경로 (선택 사항)", + "orgPolicyUpdate": "정책 업데이트", + "orgPolicyAdd": "정책 추가", + "orgPolicyConfig": "조직에 대한 접근을 구성하십시오.", + "idpUpdatedDescription": "아이덴티티 제공자가 성공적으로 업데이트되었습니다", + "redirectUrl": "리디렉션 URL", + "redirectUrlAbout": "리디렉션 URL에 대한 정보", + "redirectUrlAboutDescription": "사용자가 인증 후 리디렉션될 URL입니다. 이 URL을 신원 제공자 설정에서 구성해야 합니다.", + "pangolinAuth": "인증 - 판골린", + "verificationCodeLengthRequirements": "인증 코드가 8자여야 합니다.", + "errorOccurred": "오류가 발생했습니다.", + "emailErrorVerify": "이메일 확인에 실패했습니다:", + "emailVerified": "이메일이 성공적으로 확인되었습니다! 리디렉션 중입니다...", + "verificationCodeErrorResend": "인증 코드를 재전송하는 데 실패했습니다:", + "verificationCodeResend": "인증 코드가 재전송되었습니다", + "verificationCodeResendDescription": "검증 코드를 귀하의 이메일 주소로 재전송했습니다. 받은 편지함을 확인해 주세요.", + "emailVerify": "이메일 확인", + "emailVerifyDescription": "이메일 주소로 전송된 인증 코드를 입력하세요.", + "verificationCode": "인증 코드", + "verificationCodeEmailSent": "귀하의 이메일 주소로 인증 코드가 전송되었습니다.", + "submit": "제출", + "emailVerifyResendProgress": "재전송 중...", + "emailVerifyResend": "코드를 받지 못하셨나요? 여기 클릭하여 재전송하세요", + "passwordNotMatch": "비밀번호가 일치하지 않습니다.", + "resourceNotFoundDescription": "접근하려는 리소스가 존재하지 않습니다.", + "pincodeRequirementsLength": "PIN은 정확히 6자리여야 합니다", + "pincodeRequirementsChars": "PIN은 숫자만 포함해야 합니다.", + "passwordRequirementsLength": "비밀번호는 최소 1자 이상이어야 합니다", + "otpEmailRequirementsLength": "OTP는 최소 1자 이상이어야 합니다", + "otpEmailSent": "OTP 전송됨", + "otpEmailSentDescription": "OTP가 귀하의 이메일로 전송되었습니다.", + "otpEmailErrorAuthenticate": "이메일로 인증에 실패했습니다", + "pincodeErrorAuthenticate": "핀코드로 인증하는 데 실패했습니다", + "passwordErrorAuthenticate": "비밀번호로 인증하는 데 실패했습니다.", + "poweredBy": "제공자", + "authenticationRequired": "인증 필요", + "authenticationMethodChoose": "{name}에 접근하기 위한 선호하는 방법을 선택하세요.", + "authenticationRequest": "{name}에 접근하려면 인증해야 합니다.", + "user": "사용자", + "pincodeInput": "6자리 PIN 코드", + "pincodeSubmit": "PIN으로 로그인", + "passwordSubmit": "비밀번호로 로그인", + "otpEmailDescription": "일회성 코드가 이 이메일로 전송됩니다.", + "otpEmailSend": "일회성 코드 전송", + "otpEmail": "일회성 비밀번호 (OTP)", + "otpEmailSubmit": "OTP 제출", + "backToEmail": "이메일로 돌아가기", + "noSupportKey": "서버가 지원 키 없이 실행되고 있습니다. 프로젝트 지원을 고려하세요!", + "accessDenied": "접근 거부", + "accessDeniedDescription": "이 리소스에 접근할 수 있는 권한이 없습니다. 이게 실수라면 관리자에게 문의해 주세요.", + "accessTokenError": "액세스 토큰 확인 중 오류 발생", + "accessGranted": "접근 허가됨", + "accessUrlInvalid": "접근 URL이 유효하지 않습니다", + "accessGrantedDescription": "이 리소스에 대한 접근이 허용되었습니다. 리디렉션 중입니다...", + "accessUrlInvalidDescription": "이 공유 액세스 URL은 유효하지 않습니다. 새로운 URL을 위해 리소스 소유자에게 문의하세요.", + "tokenInvalid": "유효하지 않은 토큰", + "pincodeInvalid": "유효하지 않은 코드", + "passwordErrorRequestReset": "재설정을 요청하는 데 실패했습니다:", + "passwordErrorReset": "비밀번호 재설정 실패:", + "passwordResetSuccess": "비밀번호가 성공적으로 재설정되었습니다! 로그인으로 돌아가기...", + "passwordReset": "비밀번호 재설정", + "passwordResetDescription": "비밀번호를 재설정하는 단계를 따르세요", + "passwordResetSent": "이 이메일 주소로 비밀번호 재설정 코드를 전송하겠습니다.", + "passwordResetCode": "코드 재설정", + "passwordResetCodeDescription": "재설정 코드를 확인하려면 이메일을 확인하세요.", + "passwordNew": "새 비밀번호", + "passwordNewConfirm": "새 비밀번호 확인", + "pincodeAuth": "인증 코드", + "pincodeSubmit2": "코드 제출", + "passwordResetSubmit": "재설정 요청", + "passwordBack": "비밀번호로 돌아가기", + "loginBack": "로그인으로 돌아가기", + "signup": "가입하기", + "loginStart": "시작하려면 로그인하세요.", + "idpOidcTokenValidating": "OIDC 토큰 검증 중", + "idpOidcTokenResponse": "OIDC 토큰 응답 검증", + "idpErrorOidcTokenValidating": "OIDC 토큰 검증 오류", + "idpConnectingTo": "{name}에 연결 중", + "idpConnectingToDescription": "귀하의 신원을 확인하는 중", + "idpConnectingToProcess": "연결 중...", + "idpConnectingToFinished": "연결됨", + "idpErrorConnectingTo": "{name}에 연결하는 데 문제가 발생했습니다. 관리자에게 문의하십시오.", + "idpErrorNotFound": "IdP를 찾을 수 없습니다.", + "inviteInvalid": "유효하지 않은 초대", + "inviteInvalidDescription": "초대 링크가 유효하지 않습니다.", + "inviteErrorWrongUser": "이 초대는 이 사용자에게 해당되지 않습니다", + "inviteErrorUserNotExists": "사용자가 존재하지 않습니다. 먼저 계정을 생성해 주세요.", + "inviteErrorLoginRequired": "초대를 수락하려면 로그인해야 합니다.", + "inviteErrorExpired": "초대가 만료되었을 수 있습니다.", + "inviteErrorRevoked": "초대가 취소되었을 수 있습니다.", + "inviteErrorTypo": "초대 링크에 오타가 있을 수 있습니다.", + "pangolinSetup": "설정 - 판골린", + "orgNameRequired": "조직 이름은 필수입니다.", + "orgIdRequired": "조직 ID가 필요합니다", + "orgErrorCreate": "조직 생성 중 오류가 발생했습니다.", + "pageNotFound": "페이지를 찾을 수 없습니다", + "pageNotFoundDescription": "앗! 찾고 있는 페이지가 존재하지 않습니다.", + "overview": "개요", + "home": "홈", + "accessControl": "액세스 제어", + "settings": "설정", + "usersAll": "모든 사용자", + "license": "라이선스", + "pangolinDashboard": "대시보드 - 판골린", + "tagsWarnCannotBeLessThanZero": "maxTags와 minTags는 0보다 작을 수 없습니다", + "tagsWarnNotAllowedAutocompleteOptions": "자동 완성 옵션에 따라 태그가 허용되지 않습니다", + "tagsWarnInvalid": "validateTag에 따라 유효하지 않은 태그입니다", + "tagWarnTooShort": "태그 {tagText}가 너무 짧습니다", + "tagWarnTooLong": "태그 {tagText}가 너무 깁니다.", + "tagsWarnReachedMaxNumber": "허용된 최대 태그 수에 도달했습니다.", + "tagWarnDuplicate": "중복 태그 {tagText}가 추가되지 않았습니다.", + "supportKeyInvalid": "유효하지 않은 키", + "supportKeyInvalidDescription": "지원자 키가 유효하지 않습니다.", + "supportKeyValid": "유효한 키", + "supportKeyValidDescription": "귀하의 후원자 키가 검증되었습니다. 지원해 주셔서 감사합니다!", + "supportKeyErrorValidationDescription": "서포터 키 유효성 검사에 실패했습니다.", + "supportKey": "개발 지원 및 판골린을 입양하세요!", + "supportKeyDescription": "커뮤니티를 위해 Pangolin 개발을 지속할 수 있도록 후원자 키를 구매하세요. 귀하의 기여는 모든 사용자를 위해 애플리케이션을 유지하고 새로운 기능을 추가하는 데 더 많은 시간을 할애할 수 있게 해줍니다. 우리는 절대 이 기능을 유료화하는 데 사용하지 않을 것입니다. 이는 상업용 에디션과는 별개입니다.", + "supportKeyPet": "자신만의 애완 판골린을 입양하고 만날 수 있습니다!", + "supportKeyPurchase": "결제는 GitHub를 통해 처리됩니다. 이후, 키를 다음에서 검색할 수 있습니다.", + "supportKeyPurchaseLink": "우리 웹사이트", + "supportKeyPurchase2": "여기에서 사용하세요.", + "supportKeyLearnMore": "자세히 알아보기.", + "supportKeyOptions": "가장 적합한 옵션을 선택해 주세요.", + "supportKetOptionFull": "전체 후원자", + "forWholeServer": "전체 서버에 대해", + "lifetimePurchase": "평생 구매", + "supporterStatus": "후원자 상태", + "buy": "구매", + "supportKeyOptionLimited": "제한된 후원자", + "forFiveUsers": "5명 이하의 사용자에 대해", + "supportKeyRedeem": "서포터 키 사용", + "supportKeyHideSevenDays": "7일 동안 숨기기", + "supportKeyEnter": "지원자 키 입력", + "supportKeyEnterDescription": "당신만의 펭귄 애완동물을 만나보세요!", + "githubUsername": "GitHub 사용자 이름", + "supportKeyInput": "후원자 키", + "supportKeyBuy": "서포터 키 구매", + "logoutError": "로그아웃 중 오류 발생", + "signingAs": "로그인한 사용자", + "serverAdmin": "서버 관리자", + "licenseTierProfessionalRequired": "전문 에디션이 필요합니다.", + "licenseTierProfessionalRequiredDescription": "이 기능은 Professional Edition에서만 사용할 수 있습니다.", + "actionGetOrg": "조직 가져오기", + "actionUpdateOrg": "조직 업데이트", + "actionGetOrgUser": "조직 사용자 가져오기", + "actionListOrgDomains": "조직 도메인 목록", + "actionCreateSite": "사이트 생성", + "actionDeleteSite": "사이트 삭제", + "actionGetSite": "사이트 가져오기", + "actionListSites": "사이트 목록", + "actionUpdateSite": "사이트 업데이트", + "actionListSiteRoles": "허용된 사이트 역할 목록", + "actionCreateResource": "리소스 생성", + "actionDeleteResource": "리소스 삭제", + "actionGetResource": "리소스 가져오기", + "actionListResource": "리소스 목록", + "actionUpdateResource": "리소스 업데이트", + "actionListResourceUsers": "리소스 사용자 목록", + "actionSetResourceUsers": "리소스 사용자 설정", + "actionSetAllowedResourceRoles": "허용된 리소스 역할 설정", + "actionListAllowedResourceRoles": "허용된 리소스 역할 목록", + "actionSetResourcePassword": "리소스 비밀번호 설정", + "actionSetResourcePincode": "리소스 핀코드 설정", + "actionSetResourceEmailWhitelist": "리소스 이메일 화이트리스트 설정", + "actionGetResourceEmailWhitelist": "리소스 이메일 화이트리스트 가져오기", + "actionCreateTarget": "대상 만들기", + "actionDeleteTarget": "대상 삭제", + "actionGetTarget": "대상 가져오기", + "actionListTargets": "대상 목록", + "actionUpdateTarget": "대상 업데이트", + "actionCreateRole": "역할 생성", + "actionDeleteRole": "역할 삭제", + "actionGetRole": "역할 가져오기", + "actionListRole": "역할 목록", + "actionUpdateRole": "역할 업데이트", + "actionListAllowedRoleResources": "허용된 역할 리소스 목록", + "actionInviteUser": "사용자 초대", + "actionRemoveUser": "사용자 제거", + "actionListUsers": "사용자 목록", + "actionAddUserRole": "사용자 역할 추가", + "containersIn": "{siteName}의 컨테이너", + "actionGenerateAccessToken": "액세스 토큰 생성", + "actionDeleteAccessToken": "액세스 토큰 삭제", + "actionListAccessTokens": "액세스 토큰 목록", + "actionListResourceRules": "리소스 규칙 목록", + "actionUpdateResourceRule": "리소스 규칙 업데이트", + "actionListOrgs": "조직 목록", + "actionCreateOrg": "조직 생성", + "actionDeleteOrg": "조직 삭제", + "actionListApiKeys": "API 키 목록", + "actionListApiKeyActions": "API 키 작업 목록", + "actionSetApiKeyActions": "API 키 허용 작업 설정", + "actionCreateApiKey": "API 키 생성", + "actionDeleteApiKey": "API 키 삭제", + "actionCreateIdp": "IDP 생성", + "actionUpdateIdp": "IDP 업데이트", + "actionDeleteIdp": "IDP 삭제", + "actionListIdps": "IDP 목록", + "actionGetIdp": "IDP 가져오기", + "actionCreateIdpOrg": "IDP 조직 정책 생성", + "actionDeleteIdpOrg": "IDP 조직 정책 삭제", + "actionListIdpOrgs": "IDP 조직 목록", + "actionUpdateIdpOrg": "IDP 조직 업데이트", + "noneSelected": "선택된 항목 없음", + "orgNotFound2": "조직이 없습니다.", + "searchProgress": "검색...", + "create": "생성", + "orgs": "조직", + "loginError": "로그인 중 오류가 발생했습니다", + "passwordForgot": "비밀번호를 잊으셨나요?", + "otpAuth": "이중 인증", + "otpAuthDescription": "인증 앱에서 코드를 입력하거나 단일 사용 백업 코드 중 하나를 입력하세요.", + "otpAuthSubmit": "코드 제출", + "idpContinue": "또는 계속 진행하십시오.", + "otpAuthBack": "로그인으로 돌아가기", + "navbar": "탐색 메뉴", + "navbarDescription": "애플리케이션의 주요 탐색 메뉴", + "navbarDocsLink": "문서", + "commercialEdition": "상업용 에디션", + "otpErrorEnable": "2FA를 활성화할 수 없습니다.", + "otpErrorEnableDescription": "2FA를 활성화하는 동안 오류가 발생했습니다", + "otpSetupCheckCode": "6자리 코드를 입력하세요", + "otpSetupCheckCodeRetry": "유효하지 않은 코드입니다. 다시 시도하세요.", + "otpSetup": "이중 인증 활성화", + "otpSetupDescription": "추가 보호 계층으로 계정을 안전하게 유지하세요.", + "otpSetupScanQr": "인증 앱으로 이 QR 코드를 스캔하거나 비밀 키를 수동으로 입력하십시오:", + "otpSetupSecretCode": "인증 코드", + "otpSetupSuccess": "이중 인증 활성화됨", + "otpSetupSuccessStoreBackupCodes": "귀하의 계정이 이제 더 안전해졌습니다. 백업 코드를 저장하는 것을 잊지 마세요.", + "otpErrorDisable": "2FA를 비활성화할 수 없습니다.", + "otpErrorDisableDescription": "2FA를 비활성화하는 동안 오류가 발생했습니다.", + "otpRemove": "이중 인증 비활성화", + "otpRemoveDescription": "계정에 대한 이중 인증 비활성화", + "otpRemoveSuccess": "이중 인증 비활성화", + "otpRemoveSuccessMessage": "이중 인증이 귀하의 계정에서 비활성화되었습니다. 언제든지 다시 활성화할 수 있습니다.", + "otpRemoveSubmit": "2FA 비활성화", + "paginator": "페이지 {current} / {last}", + "paginatorToFirst": "첫 페이지로 이동", + "paginatorToPrevious": "이전 페이지로 이동", + "paginatorToNext": "다음 페이지로 이동", + "paginatorToLast": "마지막 페이지로 이동", + "copyText": "텍스트 복사", + "copyTextFailed": "텍스트 복사 실패: ", + "copyTextClipboard": "클립보드에 복사", + "inviteErrorInvalidConfirmation": "유효하지 않은 확인", + "passwordRequired": "비밀번호는 필수입니다.", + "allowAll": "모두 허용", + "permissionsAllowAll": "모든 권한 허용", + "githubUsernameRequired": "GitHub 사용자 이름이 필요합니다.", + "supportKeyRequired": "지원자 키가 필요합니다.", + "passwordRequirementsChars": "비밀번호는 최소 8자 이상이어야 합니다", + "language": "언어", + "verificationCodeRequired": "코드가 필요합니다.", + "userErrorNoUpdate": "업데이트할 사용자가 없습니다", + "siteErrorNoUpdate": "업데이트할 사이트가 없습니다.", + "resourceErrorNoUpdate": "업데이트할 리소스가 없습니다", + "authErrorNoUpdate": "업데이트할 인증 정보가 없습니다.", + "orgErrorNoUpdate": "업데이트할 조직이 없습니다.", + "orgErrorNoProvided": "제공된 조직이 없습니다.", + "apiKeysErrorNoUpdate": "업데이트할 API 키가 없습니다.", + "sidebarOverview": "개요", + "sidebarHome": "홈", + "sidebarSites": "사이트", + "sidebarResources": "리소스", + "sidebarAccessControl": "액세스 제어", + "sidebarUsers": "사용자", + "sidebarInvitations": "초대", + "sidebarRoles": "역할", + "sidebarShareableLinks": "공유 가능한 링크", + "sidebarIdentityProviders": "신원 공급자", + "sidebarLicense": "라이선스", + "enableDockerSocket": "Docker 소켓 활성화", + "enableDockerSocketDescription": "컨테이너 정보를 채우기 위해 Docker 소켓 검색을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.", + "enableDockerSocketLink": "자세히 알아보기", + "viewDockerContainers": "도커 컨테이너 보기", + "selectContainerDescription": "이 대상을 위한 호스트 이름으로 사용할 컨테이너를 선택하세요. 포트를 사용하려면 포트를 클릭하세요.", + "containerName": "이름", + "containerImage": "이미지", + "containerState": "주", + "containerNetworks": "네트워크", + "containerHostnameIp": "호스트 이름/IP", + "containerLabels": "레이블", + "containerLabelsCount": "{count, plural, one {# 레이블} other {# 레이블}}", + "containerLabelsTitle": "컨테이너 레이블", + "containerLabelEmpty": "<비어 있음>", + "containerPorts": "포트", + "containerPortsMore": "+{count}개 더", + "containerActions": "작업", + "select": "선택", + "noContainersMatchingFilters": "현재 필터와 일치하는 컨테이너를 찾을 수 없습니다.", + "showContainersWithoutPorts": "포트가 없는 컨테이너 표시", + "showStoppedContainers": "중지된 컨테이너 표시", + "noContainersFound": "컨테이너를 찾을 수 없습니다. Docker 컨테이너가 실행 중인지 확인하십시오.", + "searchContainersPlaceholder": "{count}개의 컨테이너에서 검색...", + "searchResultsCount": "{count, plural, one {# 결과} other {# 결과}}", + "filters": "필터", + "filterOptions": "필터 옵션", + "filterPorts": "포트", + "filterStopped": "중지됨", + "clearAllFilters": "모든 필터 지우기", + "columns": "열", + "toggleColumns": "열 전환", + "refreshContainersList": "컨테이너 목록 새로 고침", + "searching": "검색 중...", + "noContainersFoundMatching": "\"{filter}\"와 일치하는 컨테이너를 찾을 수 없습니다.", + "light": "빛", + "dark": "어두운", + "system": "시스템", + "theme": "테마", + "initialSetupTitle": "초기 서버 설정", + "initialSetupDescription": "초기 서버 관리자 계정을 생성하세요. 서버 관리자 계정은 하나만 존재할 수 있습니다. 이러한 자격 증명은 나중에 언제든지 변경할 수 있습니다.", + "createAdminAccount": "관리자 계정 생성", + "setupErrorCreateAdmin": "서버 관리자 계정을 생성하는 동안 오류가 발생했습니다." +} diff --git a/package-lock.json b/package-lock.json index 5adc125f..e0f2d0dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,11 +31,11 @@ "@radix-ui/react-switch": "1.2.5", "@radix-ui/react-tabs": "1.1.12", "@radix-ui/react-toast": "1.2.14", - "@react-email/components": "0.1.0", + "@react-email/components": "0.3.1", "@react-email/render": "^1.1.2", - "@react-email/tailwind": "1.0.5", "@simplewebauthn/browser": "^13.1.0", "@simplewebauthn/server": "^9.0.3", + "@react-email/tailwind": "1.2.1", "@tailwindcss/forms": "^0.5.10", "@tanstack/react-table": "8.21.3", "arctic": "^3.7.0", @@ -51,8 +51,8 @@ "cors": "2.8.5", "crypto-js": "^4.2.0", "drizzle-orm": "0.44.2", - "eslint": "9.29.0", - "eslint-config-next": "15.3.4", + "eslint": "9.31.0", + "eslint-config-next": "15.3.5", "express": "4.21.2", "express-rate-limit": "7.5.1", "glob": "11.0.3", @@ -63,14 +63,14 @@ "jmespath": "^0.16.0", "js-yaml": "4.1.0", "jsonwebtoken": "^9.0.2", - "lucide-react": "0.522.0", + "lucide-react": "0.525.0", "moment": "2.30.1", - "next": "15.3.4", - "next-intl": "^4.1.0", + "next": "15.3.5", + "next-intl": "^4.3.4", "next-themes": "0.4.6", "node-cache": "5.1.2", "node-fetch": "3.3.2", - "nodemailer": "7.0.3", + "nodemailer": "7.0.5", "npm": "^11.4.2", "oslo": "1.2.1", "pg": "^8.16.2", @@ -78,24 +78,24 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-easy-sort": "^1.6.0", - "react-hook-form": "7.58.1", + "react-hook-form": "7.60.0", "react-icons": "^5.5.0", "rebuild": "0.1.2", "semver": "^7.7.2", "swagger-ui-express": "^5.0.1", "tailwind-merge": "3.3.1", - "tw-animate-css": "^1.3.3", + "tw-animate-css": "^1.3.5", "uuid": "^11.1.0", "vaul": "1.1.2", "winston": "3.17.0", "winston-daily-rotate-file": "5.0.0", - "ws": "8.18.2", + "ws": "8.18.3", "yargs": "18.0.0", - "zod": "3.25.67", + "zod": "3.25.76", "zod-validation-error": "3.5.2" }, "devDependencies": { - "@dotenvx/dotenvx": "1.45.1", + "@dotenvx/dotenvx": "1.47.3", "@esbuild-plugins/tsconfig-paths": "0.1.2", "@tailwindcss/postcss": "^4.1.10", "@types/better-sqlite3": "7.6.12", @@ -115,16 +115,16 @@ "@types/swagger-ui-express": "^4.1.8", "@types/ws": "8.18.1", "@types/yargs": "17.0.33", - "drizzle-kit": "0.31.2", - "esbuild": "0.25.5", + "drizzle-kit": "0.31.4", + "esbuild": "0.25.6", "esbuild-node-externals": "1.18.0", "postcss": "^8", - "react-email": "4.0.16", + "react-email": "4.1.0", "tailwindcss": "^4.1.4", "tsc-alias": "1.8.16", "tsx": "4.20.3", "typescript": "^5", - "typescript-eslint": "^8.35.0" + "typescript-eslint": "^8.36.0" } }, "node_modules/@alloc/quick-lru": { @@ -313,9 +313,9 @@ } }, "node_modules/@dotenvx/dotenvx": { - "version": "1.45.1", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.45.1.tgz", - "integrity": "sha512-wKHPD+/NMMJVBPg3i98uD9jsURDy+Ck6RQRiWf39TlOAzC+Ge1FkmDk3sgeljYZxA3qF6E7SJmvRqC70XQuuVA==", + "version": "1.47.3", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.47.3.tgz", + "integrity": "sha512-V0jxoEgyTrP6INJYBXxR6qkaS1qUXmrWTz7FZVx706TgXnMnR7LVRi5Bf9z/o0UmZlkavJD13PLediPi4QvUTQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -330,8 +330,7 @@ "which": "^4.0.0" }, "bin": { - "dotenvx": "src/cli/dotenvx.js", - "git-dotenvx": "src/cli/dotenvx.js" + "dotenvx": "src/cli/dotenvx.js" }, "funding": { "url": "https://dotenvx.com" @@ -843,9 +842,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", - "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", + "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", "cpu": [ "ppc64" ], @@ -860,9 +859,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", - "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", + "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", "cpu": [ "arm" ], @@ -877,9 +876,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", - "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", + "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", "cpu": [ "arm64" ], @@ -894,9 +893,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", - "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", + "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", "cpu": [ "x64" ], @@ -911,9 +910,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", - "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", + "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", "cpu": [ "arm64" ], @@ -928,9 +927,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", - "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", + "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", "cpu": [ "x64" ], @@ -945,9 +944,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", - "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", + "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", "cpu": [ "arm64" ], @@ -962,9 +961,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", - "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", + "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", "cpu": [ "x64" ], @@ -979,9 +978,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", - "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", + "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", "cpu": [ "arm" ], @@ -996,9 +995,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", - "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", + "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", "cpu": [ "arm64" ], @@ -1013,9 +1012,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", - "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", + "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", "cpu": [ "ia32" ], @@ -1030,9 +1029,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", - "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", + "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", "cpu": [ "loong64" ], @@ -1047,9 +1046,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", - "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", + "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", "cpu": [ "mips64el" ], @@ -1064,9 +1063,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", - "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", + "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", "cpu": [ "ppc64" ], @@ -1081,9 +1080,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", - "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", + "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", "cpu": [ "riscv64" ], @@ -1098,9 +1097,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", - "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", + "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", "cpu": [ "s390x" ], @@ -1115,9 +1114,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", + "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", "cpu": [ "x64" ], @@ -1132,9 +1131,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", - "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", + "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", "cpu": [ "arm64" ], @@ -1149,9 +1148,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", - "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", + "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", "cpu": [ "x64" ], @@ -1166,9 +1165,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", - "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", + "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", "cpu": [ "arm64" ], @@ -1183,9 +1182,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", - "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", + "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", "cpu": [ "x64" ], @@ -1199,10 +1198,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", + "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", - "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", + "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", "cpu": [ "x64" ], @@ -1217,9 +1233,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", - "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", + "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", "cpu": [ "arm64" ], @@ -1234,9 +1250,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", - "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", + "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", "cpu": [ "ia32" ], @@ -1251,9 +1267,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", - "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", + "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", "cpu": [ "x64" ], @@ -1307,9 +1323,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz", - "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.6", @@ -1321,18 +1337,18 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz", - "integrity": "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" @@ -1365,9 +1381,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.29.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz", - "integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1398,18 +1414,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@floating-ui/core": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz", @@ -2089,24 +2093,24 @@ } }, "node_modules/@next/env": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.4.tgz", - "integrity": "sha512-ZkdYzBseS6UjYzz6ylVKPOK+//zLWvD6Ta+vpoye8cW11AjiQjGYVibF0xuvT4L0iJfAPfZLFidaEzAOywyOAQ==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.5.tgz", + "integrity": "sha512-7g06v8BUVtN2njAX/r8gheoVffhiKFVt4nx74Tt6G4Hqw9HCLYQVx/GkH2qHvPtAHZaUNZ0VXAa0pQP6v1wk7g==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.3.4.tgz", - "integrity": "sha512-lBxYdj7TI8phbJcLSAqDt57nIcobEign5NYIKCiy0hXQhrUbTqLqOaSDi568U6vFg4hJfBdZYsG4iP/uKhCqgg==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.3.5.tgz", + "integrity": "sha512-BZwWPGfp9po/rAnJcwUBaM+yT/+yTWIkWdyDwc74G9jcfTrNrmsHe+hXHljV066YNdVs8cxROxX5IgMQGX190w==", "license": "MIT", "dependencies": { "fast-glob": "3.3.1" } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.4.tgz", - "integrity": "sha512-z0qIYTONmPRbwHWvpyrFXJd5F9YWLCsw3Sjrzj2ZvMYy9NPQMPZ1NjOJh4ojr4oQzcGYwgJKfidzehaNa1BpEg==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.5.tgz", + "integrity": "sha512-lM/8tilIsqBq+2nq9kbTW19vfwFve0NR7MxfkuSUbRSgXlMQoJYg+31+++XwKVSXk4uT23G2eF/7BRIKdn8t8w==", "cpu": [ "arm64" ], @@ -2120,9 +2124,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.4.tgz", - "integrity": "sha512-Z0FYJM8lritw5Wq+vpHYuCIzIlEMjewG2aRkc3Hi2rcbULknYL/xqfpBL23jQnCSrDUGAo/AEv0Z+s2bff9Zkw==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.5.tgz", + "integrity": "sha512-WhwegPQJ5IfoUNZUVsI9TRAlKpjGVK0tpJTL6KeiC4cux9774NYE9Wu/iCfIkL/5J8rPAkqZpG7n+EfiAfidXA==", "cpu": [ "x64" ], @@ -2136,9 +2140,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.4.tgz", - "integrity": "sha512-l8ZQOCCg7adwmsnFm8m5q9eIPAHdaB2F3cxhufYtVo84pymwKuWfpYTKcUiFcutJdp9xGHC+F1Uq3xnFU1B/7g==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.5.tgz", + "integrity": "sha512-LVD6uMOZ7XePg3KWYdGuzuvVboxujGjbcuP2jsPAN3MnLdLoZUXKRc6ixxfs03RH7qBdEHCZjyLP/jBdCJVRJQ==", "cpu": [ "arm64" ], @@ -2152,9 +2156,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.4.tgz", - "integrity": "sha512-wFyZ7X470YJQtpKot4xCY3gpdn8lE9nTlldG07/kJYexCUpX1piX+MBfZdvulo+t1yADFVEuzFfVHfklfEx8kw==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.5.tgz", + "integrity": "sha512-k8aVScYZ++BnS2P69ClK7v4nOu702jcF9AIHKu6llhHEtBSmM2zkPGl9yoqbSU/657IIIb0QHpdxEr0iW9z53A==", "cpu": [ "arm64" ], @@ -2168,9 +2172,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.4.tgz", - "integrity": "sha512-gEbH9rv9o7I12qPyvZNVTyP/PWKqOp8clvnoYZQiX800KkqsaJZuOXkWgMa7ANCCh/oEN2ZQheh3yH8/kWPSEg==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.5.tgz", + "integrity": "sha512-2xYU0DI9DGN/bAHzVwADid22ba5d/xrbrQlr2U+/Q5WkFUzeL0TDR963BdrtLS/4bMmKZGptLeg6282H/S2i8A==", "cpu": [ "x64" ], @@ -2184,9 +2188,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.4.tgz", - "integrity": "sha512-Cf8sr0ufuC/nu/yQ76AnarbSAXcwG/wj+1xFPNbyNo8ltA6kw5d5YqO8kQuwVIxk13SBdtgXrNyom3ZosHAy4A==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.5.tgz", + "integrity": "sha512-TRYIqAGf1KCbuAB0gjhdn5Ytd8fV+wJSM2Nh2is/xEqR8PZHxfQuaiNhoF50XfY90sNpaRMaGhF6E+qjV1b9Tg==", "cpu": [ "x64" ], @@ -2200,9 +2204,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.4.tgz", - "integrity": "sha512-ay5+qADDN3rwRbRpEhTOreOn1OyJIXS60tg9WMYTWCy3fB6rGoyjLVxc4dR9PYjEdR2iDYsaF5h03NA+XuYPQQ==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.5.tgz", + "integrity": "sha512-h04/7iMEUSMY6fDGCvdanKqlO1qYvzNxntZlCzfE8i5P0uqzVQWQquU1TIhlz0VqGQGXLrFDuTJVONpqGqjGKQ==", "cpu": [ "arm64" ], @@ -2216,9 +2220,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.4.tgz", - "integrity": "sha512-4kDt31Bc9DGyYs41FTL1/kNpDeHyha2TC0j5sRRoKCyrhNcfZ/nRQkAUlF27mETwm8QyHqIjHJitfcza2Iykfg==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.5.tgz", + "integrity": "sha512-5fhH6fccXxnX2KhllnGhkYMndhOiLOLEiVGYjP2nizqeGWkN10sA9taATlXwake2E2XMvYZjjz0Uj7T0y+z1yw==", "cpu": [ "x64" ], @@ -3999,9 +4003,9 @@ } }, "node_modules/@react-email/button": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.1.0.tgz", - "integrity": "sha512-fg4LtgTu5zXxaRSly9cuv6sHVF/hi1lElbRaIA8EPx5coWOBhCto6rCPfawcXpaN2oER7rNHUrcNBkI+lz5F9A==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.2.0.tgz", + "integrity": "sha512-8i+v6cMxr2emz4ihCrRiYJPp2/sdYsNNsBzXStlcA+/B9Umpm5Jj3WJKYpgTPM+aeyiqlG/MMI1AucnBm4f1oQ==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -4050,13 +4054,13 @@ } }, "node_modules/@react-email/components": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.1.0.tgz", - "integrity": "sha512-Rx0eZk0XuzLKXC5NoMm8xuH72ALVsPYNb/BvcdCJx4EZAoVpQISb4sCqpo9blVYVIazNr4MqWroqFb3ZNrCLMQ==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.3.1.tgz", + "integrity": "sha512-FqcyGaUpJJu8zfYGSS+qaSy7Zc2BFAswBc/LvHeSV4iTQMZMD8Dy7aS/NvP1SQMg5vjsO1aMpGFdrD4NBY58dw==", "license": "MIT", "dependencies": { "@react-email/body": "0.0.11", - "@react-email/button": "0.1.0", + "@react-email/button": "0.2.0", "@react-email/code-block": "0.1.0", "@react-email/code-inline": "0.0.5", "@react-email/column": "0.0.13", @@ -4070,10 +4074,10 @@ "@react-email/link": "0.0.12", "@react-email/markdown": "0.0.15", "@react-email/preview": "0.0.13", - "@react-email/render": "1.1.2", + "@react-email/render": "1.1.3", "@react-email/row": "0.0.12", "@react-email/section": "0.0.16", - "@react-email/tailwind": "1.0.5", + "@react-email/tailwind": "1.2.1", "@react-email/text": "0.1.5" }, "engines": { @@ -4083,24 +4087,6 @@ "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, - "node_modules/@react-email/components/node_modules/@react-email/render": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.1.2.tgz", - "integrity": "sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw==", - "license": "MIT", - "dependencies": { - "html-to-text": "^9.0.5", - "prettier": "^3.5.3", - "react-promise-suspense": "^0.3.4" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "react": "^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" - } - }, "node_modules/@react-email/container": { "version": "0.0.15", "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.15.tgz", @@ -4264,9 +4250,9 @@ } }, "node_modules/@react-email/tailwind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-1.0.5.tgz", - "integrity": "sha512-BH00cZSeFfP9HiDASl+sPHi7Hh77W5nzDgdnxtsVr/m3uQD9g180UwxcE3PhOfx0vRdLzQUU8PtmvvDfbztKQg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-1.2.1.tgz", + "integrity": "sha512-SmVyDuNQLJwO3wHEe/snSTaRhf/Exldy5DQU/RyPjcSPC0EuXXYwFlBr16br8jJSxkZA/fL91AxKL7HbbWp0Rw==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -4869,9 +4855,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.0.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.7.tgz", - "integrity": "sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw==", + "version": "24.0.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.12.tgz", + "integrity": "sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g==", "devOptional": true, "license": "MIT", "dependencies": { @@ -4997,16 +4983,16 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz", - "integrity": "sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz", + "integrity": "sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==", "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/type-utils": "8.35.1", - "@typescript-eslint/utils": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/scope-manager": "8.36.0", + "@typescript-eslint/type-utils": "8.36.0", + "@typescript-eslint/utils": "8.36.0", + "@typescript-eslint/visitor-keys": "8.36.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -5020,7 +5006,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.35.1", + "@typescript-eslint/parser": "^8.36.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -5035,15 +5021,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.1.tgz", - "integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.36.0.tgz", + "integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==", "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/typescript-estree": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/scope-manager": "8.36.0", + "@typescript-eslint/types": "8.36.0", + "@typescript-eslint/typescript-estree": "8.36.0", + "@typescript-eslint/visitor-keys": "8.36.0", "debug": "^4.3.4" }, "engines": { @@ -5059,13 +5045,13 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.1.tgz", - "integrity": "sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz", + "integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==", "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.35.1", - "@typescript-eslint/types": "^8.35.1", + "@typescript-eslint/tsconfig-utils": "^8.36.0", + "@typescript-eslint/types": "^8.36.0", "debug": "^4.3.4" }, "engines": { @@ -5080,13 +5066,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.1.tgz", - "integrity": "sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz", + "integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1" + "@typescript-eslint/types": "8.36.0", + "@typescript-eslint/visitor-keys": "8.36.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5097,9 +5083,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.1.tgz", - "integrity": "sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz", + "integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5113,13 +5099,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.1.tgz", - "integrity": "sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.36.0.tgz", + "integrity": "sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==", "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.35.1", - "@typescript-eslint/utils": "8.35.1", + "@typescript-eslint/typescript-estree": "8.36.0", + "@typescript-eslint/utils": "8.36.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -5136,9 +5122,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.1.tgz", - "integrity": "sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz", + "integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5149,15 +5135,15 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.1.tgz", - "integrity": "sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz", + "integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==", "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.35.1", - "@typescript-eslint/tsconfig-utils": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/project-service": "8.36.0", + "@typescript-eslint/tsconfig-utils": "8.36.0", + "@typescript-eslint/types": "8.36.0", + "@typescript-eslint/visitor-keys": "8.36.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -5229,15 +5215,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.1.tgz", - "integrity": "sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.36.0.tgz", + "integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/typescript-estree": "8.35.1" + "@typescript-eslint/scope-manager": "8.36.0", + "@typescript-eslint/types": "8.36.0", + "@typescript-eslint/typescript-estree": "8.36.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5252,12 +5238,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.1.tgz", - "integrity": "sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz", + "integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/types": "8.36.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -6251,6 +6237,16 @@ "node": ">=18" } }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -6503,6 +6499,23 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -6761,9 +6774,9 @@ } }, "node_modules/decimal.js": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", - "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "license": "MIT" }, "node_modules/decompress-response": { @@ -6976,9 +6989,9 @@ } }, "node_modules/drizzle-kit": { - "version": "0.31.2", - "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.2.tgz", - "integrity": "sha512-Z2Uqxvu4HNFzlDkG3NQ2BYpII8SlOMkpjsC5XFh9TsYP2nYhfVamVjQ8spiMFXH3vGOyUt1cQ5FZ1JSgl6+8QQ==", + "version": "0.31.4", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.4.tgz", + "integrity": "sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA==", "dev": true, "license": "MIT", "dependencies": { @@ -7476,9 +7489,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", - "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", + "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -7489,31 +7502,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.5", - "@esbuild/android-arm": "0.25.5", - "@esbuild/android-arm64": "0.25.5", - "@esbuild/android-x64": "0.25.5", - "@esbuild/darwin-arm64": "0.25.5", - "@esbuild/darwin-x64": "0.25.5", - "@esbuild/freebsd-arm64": "0.25.5", - "@esbuild/freebsd-x64": "0.25.5", - "@esbuild/linux-arm": "0.25.5", - "@esbuild/linux-arm64": "0.25.5", - "@esbuild/linux-ia32": "0.25.5", - "@esbuild/linux-loong64": "0.25.5", - "@esbuild/linux-mips64el": "0.25.5", - "@esbuild/linux-ppc64": "0.25.5", - "@esbuild/linux-riscv64": "0.25.5", - "@esbuild/linux-s390x": "0.25.5", - "@esbuild/linux-x64": "0.25.5", - "@esbuild/netbsd-arm64": "0.25.5", - "@esbuild/netbsd-x64": "0.25.5", - "@esbuild/openbsd-arm64": "0.25.5", - "@esbuild/openbsd-x64": "0.25.5", - "@esbuild/sunos-x64": "0.25.5", - "@esbuild/win32-arm64": "0.25.5", - "@esbuild/win32-ia32": "0.25.5", - "@esbuild/win32-x64": "0.25.5" + "@esbuild/aix-ppc64": "0.25.6", + "@esbuild/android-arm": "0.25.6", + "@esbuild/android-arm64": "0.25.6", + "@esbuild/android-x64": "0.25.6", + "@esbuild/darwin-arm64": "0.25.6", + "@esbuild/darwin-x64": "0.25.6", + "@esbuild/freebsd-arm64": "0.25.6", + "@esbuild/freebsd-x64": "0.25.6", + "@esbuild/linux-arm": "0.25.6", + "@esbuild/linux-arm64": "0.25.6", + "@esbuild/linux-ia32": "0.25.6", + "@esbuild/linux-loong64": "0.25.6", + "@esbuild/linux-mips64el": "0.25.6", + "@esbuild/linux-ppc64": "0.25.6", + "@esbuild/linux-riscv64": "0.25.6", + "@esbuild/linux-s390x": "0.25.6", + "@esbuild/linux-x64": "0.25.6", + "@esbuild/netbsd-arm64": "0.25.6", + "@esbuild/netbsd-x64": "0.25.6", + "@esbuild/openbsd-arm64": "0.25.6", + "@esbuild/openbsd-x64": "0.25.6", + "@esbuild/openharmony-arm64": "0.25.6", + "@esbuild/sunos-x64": "0.25.6", + "@esbuild/win32-arm64": "0.25.6", + "@esbuild/win32-ia32": "0.25.6", + "@esbuild/win32-x64": "0.25.6" } }, "node_modules/esbuild-node-externals": { @@ -7573,18 +7587,18 @@ } }, "node_modules/eslint": { - "version": "9.29.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz", - "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.1", - "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.14.0", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.29.0", + "@eslint/js": "9.31.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -7633,12 +7647,12 @@ } }, "node_modules/eslint-config-next": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.3.4.tgz", - "integrity": "sha512-WqeumCq57QcTP2lYlV6BRUySfGiBYEXlQ1L0mQ+u4N4X4ZhUVSSQ52WtjqHv60pJ6dD7jn+YZc0d1/ZSsxccvg==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.3.5.tgz", + "integrity": "sha512-oQdvnIgP68wh2RlR3MdQpvaJ94R6qEFl+lnu8ZKxPj5fsAHrSF/HlAOZcsimLw3DT6bnEQIUdbZC2Ab6sWyptg==", "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.3.4", + "@next/eslint-plugin-next": "15.3.5", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", @@ -8103,6 +8117,13 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -9664,6 +9685,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", @@ -10068,9 +10099,9 @@ } }, "node_modules/lucide-react": { - "version": "0.522.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.522.0.tgz", - "integrity": "sha512-jnJbw974yZ7rQHHEFKJOlWAefG3ATSCZHANZxIdx8Rk/16siuwjgA4fBULpXEAWx/RlTs3FzmKW/udWUuO0aRw==", + "version": "0.525.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz", + "integrity": "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -10436,12 +10467,12 @@ } }, "node_modules/next": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/next/-/next-15.3.4.tgz", - "integrity": "sha512-mHKd50C+mCjam/gcnwqL1T1vPx/XQNFlXqFIVdgQdVAFY9iIQtY0IfaVflEYzKiqjeA7B0cYYMaCrmAYFjs4rA==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/next/-/next-15.3.5.tgz", + "integrity": "sha512-RkazLBMMDJSJ4XZQ81kolSpwiCt907l0xcgcpF4xC2Vml6QVcPNXW0NQRwQ80FFtSn7UM52XN0anaw8TEJXaiw==", "license": "MIT", "dependencies": { - "@next/env": "15.3.4", + "@next/env": "15.3.5", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", @@ -10456,14 +10487,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.3.4", - "@next/swc-darwin-x64": "15.3.4", - "@next/swc-linux-arm64-gnu": "15.3.4", - "@next/swc-linux-arm64-musl": "15.3.4", - "@next/swc-linux-x64-gnu": "15.3.4", - "@next/swc-linux-x64-musl": "15.3.4", - "@next/swc-win32-arm64-msvc": "15.3.4", - "@next/swc-win32-x64-msvc": "15.3.4", + "@next/swc-darwin-arm64": "15.3.5", + "@next/swc-darwin-x64": "15.3.5", + "@next/swc-linux-arm64-gnu": "15.3.5", + "@next/swc-linux-arm64-musl": "15.3.5", + "@next/swc-linux-x64-gnu": "15.3.5", + "@next/swc-linux-x64-musl": "15.3.5", + "@next/swc-win32-arm64-msvc": "15.3.5", + "@next/swc-win32-x64-msvc": "15.3.5", "sharp": "^0.34.1" }, "peerDependencies": { @@ -10490,9 +10521,9 @@ } }, "node_modules/next-intl": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.3.1.tgz", - "integrity": "sha512-FylHpOoQw5MpOyJt4cw8pNEGba7r3jKDSqt112fmBqXVceGR5YncmqpxS5MvSHsWRwbjqpOV8OsZCIY/4f4HWg==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.3.4.tgz", + "integrity": "sha512-VWLIDlGbnL/o4LnveJTJD1NOYN8lh3ZAGTWw2krhfgg53as3VsS4jzUVnArJdqvwtlpU/2BIDbWTZ7V4o1jFEw==", "funding": [ { "type": "individual", @@ -10503,7 +10534,7 @@ "dependencies": { "@formatjs/intl-localematcher": "^0.5.4", "negotiator": "^1.0.0", - "use-intl": "^4.3.1" + "use-intl": "^4.3.4" }, "peerDependencies": { "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", @@ -10626,9 +10657,9 @@ } }, "node_modules/nodemailer": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz", - "integrity": "sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.5.tgz", + "integrity": "sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -13121,6 +13152,26 @@ "inBundle": true, "license": "ISC" }, + "node_modules/nypm": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.0.tgz", + "integrity": "sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "pathe": "^2.0.3", + "pkg-types": "^2.0.0", + "tinyexec": "^0.3.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -13864,6 +13915,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/peberminta": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", @@ -13980,6 +14038,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.2.0.tgz", + "integrity": "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, "node_modules/plimit-lit": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz", @@ -14129,6 +14199,20 @@ "node": ">=6" } }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -14343,9 +14427,9 @@ "license": "0BSD" }, "node_modules/react-email": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/react-email/-/react-email-4.0.16.tgz", - "integrity": "sha512-auhFU+nQxAkKkP6lQhPyGsa9exwfUEzp2BwZnjHokCwphZlg30tu4t1LgdKRwGPYsi7XNGy6asbVLAUhOVpzzg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-4.1.0.tgz", + "integrity": "sha512-UvG5z1/gNOsLNwKPO87vgMoF7tdzUGd0kIy4fozzdBBsyLUju7hNVLBRm9j+Li/CwP5CXFT8Y5jZBtIFvSyr0w==", "dev": true, "license": "MIT", "dependencies": { @@ -14357,15 +14441,18 @@ "debounce": "^2.0.0", "esbuild": "^0.25.0", "glob": "^11.0.0", + "jiti": "2.4.2", "log-symbols": "^7.0.0", "mime-types": "^3.0.0", - "next": "^15.3.1", "normalize-path": "^3.0.0", + "nypm": "0.6.0", "ora": "^8.0.0", - "socket.io": "^4.8.1" + "prompts": "2.4.2", + "socket.io": "^4.8.1", + "tsconfig-paths": "4.2.0" }, "bin": { - "email": "dist/cli/index.mjs" + "email": "dist/index.js" }, "engines": { "node": ">=18.0.0" @@ -14394,6 +14481,19 @@ "node": ">=18" } }, + "node_modules/react-email/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/react-email/node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -14417,10 +14517,25 @@ "node": ">= 0.6" } }, + "node_modules/react-email/node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/react-hook-form": { - "version": "7.58.1", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.58.1.tgz", - "integrity": "sha512-Lml/KZYEEFfPhUVgE0RdCVpnC4yhW+PndRhbiTtdvSlQTL8IfVR+iQkBjLIvmmc6+GGoVeM11z37ktKFPAb0FA==", + "version": "7.60.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.60.0.tgz", + "integrity": "sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -15161,6 +15276,13 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -15786,6 +15908,13 @@ "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "license": "MIT" }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", @@ -16127,15 +16256,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.1.tgz", - "integrity": "sha512-xslJjFzhOmHYQzSB/QTeASAHbjmxOGEP6Coh93TXmUBFQoJ1VU35UHIDmG06Jd6taf3wqqC1ntBnCMeymy5Ovw==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.36.0.tgz", + "integrity": "sha512-fTCqxthY+h9QbEgSIBfL9iV6CvKDFuoxg6bHPNpJ9HIUzS+jy2lCEyCmGyZRWEBSaykqcDPf1SJ+BfCI8DRopA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.35.1", - "@typescript-eslint/parser": "8.35.1", - "@typescript-eslint/utils": "8.35.1" + "@typescript-eslint/eslint-plugin": "8.36.0", + "@typescript-eslint/parser": "8.36.0", + "@typescript-eslint/utils": "8.36.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -16248,9 +16377,9 @@ } }, "node_modules/use-intl": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.3.1.tgz", - "integrity": "sha512-8Xn5RXzeHZhWqqZimi1wi2pKFqm0NxRUOB41k1QdjbPX+ysoeLW3Ey+fi603D/e5EGb0fYw8WzjgtUagJdlIvg==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.3.4.tgz", + "integrity": "sha512-sHfiU0QeJ1rirNWRxvCyvlSh9+NczcOzRnPyMeo2rtHXhVnBsvMRjE+UG4eh3lRhCxrvcqei/I0lBxsc59on1w==", "license": "MIT", "dependencies": { "@formatjs/fast-memoize": "^2.2.0", @@ -16635,9 +16764,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -16770,9 +16899,9 @@ } }, "node_modules/zod": { - "version": "3.25.67", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", - "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 281d614d..04851288 100644 --- a/package.json +++ b/package.json @@ -49,11 +49,11 @@ "@radix-ui/react-switch": "1.2.5", "@radix-ui/react-tabs": "1.1.12", "@radix-ui/react-toast": "1.2.14", - "@react-email/components": "0.1.0", + "@react-email/components": "0.3.1", "@react-email/render": "^1.1.2", - "@react-email/tailwind": "1.0.5", "@simplewebauthn/browser": "^13.1.0", "@simplewebauthn/server": "^9.0.3", + "@react-email/tailwind": "1.2.1", "@tailwindcss/forms": "^0.5.10", "@tanstack/react-table": "8.21.3", "arctic": "^3.7.0", @@ -69,8 +69,8 @@ "cors": "2.8.5", "crypto-js": "^4.2.0", "drizzle-orm": "0.44.2", - "eslint": "9.29.0", - "eslint-config-next": "15.3.4", + "eslint": "9.31.0", + "eslint-config-next": "15.3.5", "express": "4.21.2", "express-rate-limit": "7.5.1", "glob": "11.0.3", @@ -81,14 +81,14 @@ "jmespath": "^0.16.0", "js-yaml": "4.1.0", "jsonwebtoken": "^9.0.2", - "lucide-react": "0.522.0", + "lucide-react": "0.525.0", "moment": "2.30.1", - "next": "15.3.4", - "next-intl": "^4.1.0", + "next": "15.3.5", + "next-intl": "^4.3.4", "next-themes": "0.4.6", "node-cache": "5.1.2", "node-fetch": "3.3.2", - "nodemailer": "7.0.3", + "nodemailer": "7.0.5", "npm": "^11.4.2", "oslo": "1.2.1", "pg": "^8.16.2", @@ -96,24 +96,24 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-easy-sort": "^1.6.0", - "react-hook-form": "7.58.1", + "react-hook-form": "7.60.0", "react-icons": "^5.5.0", "rebuild": "0.1.2", "semver": "^7.7.2", "swagger-ui-express": "^5.0.1", "tailwind-merge": "3.3.1", - "tw-animate-css": "^1.3.3", + "tw-animate-css": "^1.3.5", "uuid": "^11.1.0", "vaul": "1.1.2", "winston": "3.17.0", "winston-daily-rotate-file": "5.0.0", - "ws": "8.18.2", - "zod": "3.25.67", + "ws": "8.18.3", + "zod": "3.25.76", "zod-validation-error": "3.5.2", "yargs": "18.0.0" }, "devDependencies": { - "@dotenvx/dotenvx": "1.45.1", + "@dotenvx/dotenvx": "1.47.3", "@esbuild-plugins/tsconfig-paths": "0.1.2", "@tailwindcss/postcss": "^4.1.10", "@types/better-sqlite3": "7.6.12", @@ -133,16 +133,16 @@ "@types/swagger-ui-express": "^4.1.8", "@types/ws": "8.18.1", "@types/yargs": "17.0.33", - "drizzle-kit": "0.31.2", - "esbuild": "0.25.5", + "drizzle-kit": "0.31.4", + "esbuild": "0.25.6", "esbuild-node-externals": "1.18.0", "postcss": "^8", - "react-email": "4.0.16", + "react-email": "4.1.0", "tailwindcss": "^4.1.4", "tsc-alias": "1.8.16", "tsx": "4.20.3", "typescript": "^5", - "typescript-eslint": "^8.35.0" + "typescript-eslint": "^8.36.0" }, "overrides": { "emblor": { diff --git a/server/routers/auth/completeTotpSetup.ts b/server/routers/auth/completeTotpSetup.ts new file mode 100644 index 00000000..397f356b --- /dev/null +++ b/server/routers/auth/completeTotpSetup.ts @@ -0,0 +1,179 @@ +import { Request, Response, NextFunction } from "express"; +import createHttpError from "http-errors"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; +import HttpCode from "@server/types/HttpCode"; +import { response } from "@server/lib"; +import { db } from "@server/db"; +import { twoFactorBackupCodes, users } from "@server/db"; +import { eq, and } from "drizzle-orm"; +import { alphabet, generateRandomString } from "oslo/crypto"; +import { hashPassword, verifyPassword } from "@server/auth/password"; +import { verifyTotpCode } from "@server/auth/totp"; +import logger from "@server/logger"; +import { sendEmail } from "@server/emails"; +import TwoFactorAuthNotification from "@server/emails/templates/TwoFactorAuthNotification"; +import config from "@server/lib/config"; +import { UserType } from "@server/types/UserTypes"; + +export const completeTotpSetupBody = z + .object({ + email: z.string().email(), + password: z.string(), + code: z.string() + }) + .strict(); + +export type CompleteTotpSetupBody = z.infer; + +export type CompleteTotpSetupResponse = { + valid: boolean; + backupCodes?: string[]; +}; + +export async function completeTotpSetup( + req: Request, + res: Response, + next: NextFunction +): Promise { + const parsedBody = completeTotpSetupBody.safeParse(req.body); + + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { email, password, code } = parsedBody.data; + + try { + // Find the user by email + const [user] = await db + .select() + .from(users) + .where(and(eq(users.email, email), eq(users.type, UserType.Internal))) + .limit(1); + + if (!user) { + return next( + createHttpError( + HttpCode.UNAUTHORIZED, + "Invalid credentials" + ) + ); + } + + // Verify password + const validPassword = await verifyPassword(password, user.passwordHash!); + if (!validPassword) { + return next( + createHttpError( + HttpCode.UNAUTHORIZED, + "Invalid credentials" + ) + ); + } + + // Check if 2FA is enabled but not yet completed + if (!user.twoFactorEnabled) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Two-factor authentication is not required for this user" + ) + ); + } + + if (!user.twoFactorSecret) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "User has not started two-factor authentication setup" + ) + ); + } + + // Verify the TOTP code + const valid = await verifyTotpCode( + code, + user.twoFactorSecret, + user.userId + ); + + if (!valid) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Two-factor authentication code is incorrect. Email: ${email}. IP: ${req.ip}.` + ); + } + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Invalid two-factor authentication code" + ) + ); + } + + // Generate backup codes and finalize setup + let codes: string[] = []; + await db.transaction(async (trx) => { + // Note: We don't set twoFactorEnabled to true here because it's already true + // We just need to generate backup codes since the setup is now complete + + const backupCodes = await generateBackupCodes(); + codes = backupCodes; + for (const code of backupCodes) { + const hash = await hashPassword(code); + + await trx.insert(twoFactorBackupCodes).values({ + userId: user.userId, + codeHash: hash + }); + } + }); + + // Send notification email + sendEmail( + TwoFactorAuthNotification({ + email: user.email!, + enabled: true + }), + { + to: user.email!, + from: config.getRawConfig().email?.no_reply, + subject: "Two-factor authentication enabled" + } + ); + + return response(res, { + data: { + valid: true, + backupCodes: codes + }, + success: true, + error: false, + message: "Two-factor authentication setup completed successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to complete two-factor authentication setup" + ) + ); + } +} + +async function generateBackupCodes(): Promise { + const codes = []; + for (let i = 0; i < 10; i++) { + const code = generateRandomString(6, alphabet("0-9", "A-Z", "a-z")); + codes.push(code); + } + return codes; +} \ No newline at end of file diff --git a/server/routers/auth/index.ts b/server/routers/auth/index.ts index cc8fd630..48959b9c 100644 --- a/server/routers/auth/index.ts +++ b/server/routers/auth/index.ts @@ -3,6 +3,8 @@ export * from "./signup"; export * from "./logout"; export * from "./verifyTotp"; export * from "./requestTotpSecret"; +export * from "./setupTotpSecret"; +export * from "./completeTotpSetup"; export * from "./disable2fa"; export * from "./verifyEmail"; export * from "./requestEmailVerificationCode"; diff --git a/server/routers/auth/login.ts b/server/routers/auth/login.ts index 091910c1..46dd123f 100644 --- a/server/routers/auth/login.ts +++ b/server/routers/auth/login.ts @@ -36,6 +36,7 @@ export type LoginResponse = { codeRequested?: boolean; emailVerificationRequired?: boolean; useSecurityKey?: boolean; + twoFactorSetupRequired?: boolean; }; export const dynamic = "force-dynamic"; @@ -127,6 +128,17 @@ export async function login( } if (existingUser.twoFactorEnabled) { + // If 2FA is enabled but no secret exists, force setup + if (!existingUser.twoFactorSecret) { + return response(res, { + data: { twoFactorSetupRequired: true }, + success: true, + error: false, + message: "Two-factor authentication setup required", + status: HttpCode.ACCEPTED + }); + } + if (!code) { return response<{ codeRequested: boolean }>(res, { data: { codeRequested: true }, @@ -139,7 +151,7 @@ export async function login( const validOTP = await verifyTotpCode( code, - existingUser.twoFactorSecret!, + existingUser.twoFactorSecret, existingUser.userId ); diff --git a/server/routers/auth/setupTotpSecret.ts b/server/routers/auth/setupTotpSecret.ts new file mode 100644 index 00000000..89807e8e --- /dev/null +++ b/server/routers/auth/setupTotpSecret.ts @@ -0,0 +1,127 @@ +import { Request, Response, NextFunction } from "express"; +import createHttpError from "http-errors"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; +import { encodeHex } from "oslo/encoding"; +import HttpCode from "@server/types/HttpCode"; +import { response } from "@server/lib"; +import { db } from "@server/db"; +import { User, users } from "@server/db"; +import { eq, and } from "drizzle-orm"; +import { createTOTPKeyURI } from "oslo/otp"; +import logger from "@server/logger"; +import { verifyPassword } from "@server/auth/password"; +import { UserType } from "@server/types/UserTypes"; + +export const setupTotpSecretBody = z + .object({ + email: z.string().email(), + password: z.string() + }) + .strict(); + +export type SetupTotpSecretBody = z.infer; + +export type SetupTotpSecretResponse = { + secret: string; + uri: string; +}; + +export async function setupTotpSecret( + req: Request, + res: Response, + next: NextFunction +): Promise { + const parsedBody = setupTotpSecretBody.safeParse(req.body); + + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { email, password } = parsedBody.data; + + try { + // Find the user by email + const [user] = await db + .select() + .from(users) + .where(and(eq(users.email, email), eq(users.type, UserType.Internal))) + .limit(1); + + if (!user) { + return next( + createHttpError( + HttpCode.UNAUTHORIZED, + "Invalid credentials" + ) + ); + } + + // Verify password + const validPassword = await verifyPassword(password, user.passwordHash!); + if (!validPassword) { + return next( + createHttpError( + HttpCode.UNAUTHORIZED, + "Invalid credentials" + ) + ); + } + + // Check if 2FA is enabled but no secret exists (forced setup scenario) + if (!user.twoFactorEnabled) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Two-factor authentication is not required for this user" + ) + ); + } + + if (user.twoFactorSecret) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "User has already completed two-factor authentication setup" + ) + ); + } + + // Generate new TOTP secret + const hex = crypto.getRandomValues(new Uint8Array(20)); + const secret = encodeHex(hex); + const uri = createTOTPKeyURI("Pangolin", user.email!, hex); + + // Save the secret to the database + await db + .update(users) + .set({ + twoFactorSecret: secret + }) + .where(eq(users.userId, user.userId)); + + return response(res, { + data: { + secret, + uri + }, + success: true, + error: false, + message: "TOTP secret generated successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to generate TOTP secret" + ) + ); + } +} \ No newline at end of file diff --git a/server/routers/external.ts b/server/routers/external.ts index c3c2e763..b014726e 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -490,6 +490,13 @@ authenticated.put( ); authenticated.get("/org/:orgId/user/:userId", verifyOrgAccess, user.getOrgUser); +authenticated.patch( + "/org/:orgId/user/:userId/2fa", + verifyOrgAccess, + verifyUserAccess, + verifyUserHasAction(ActionsEnum.getOrgUser), + user.updateUser2FA +); authenticated.get( "/org/:orgId/users", @@ -718,6 +725,8 @@ authRouter.post( verifySessionUserMiddleware, auth.requestTotpSecret ); +authRouter.post("/2fa/setup", auth.setupTotpSecret); +authRouter.post("/2fa/complete-setup", auth.completeTotpSetup); authRouter.post("/2fa/disable", verifySessionUserMiddleware, auth.disable2fa); authRouter.post("/verify-email", verifySessionMiddleware, auth.verifyEmail); diff --git a/server/routers/user/getOrgUser.ts b/server/routers/user/getOrgUser.ts index 562ef34e..05e231c9 100644 --- a/server/routers/user/getOrgUser.ts +++ b/server/routers/user/getOrgUser.ts @@ -23,7 +23,8 @@ async function queryUser(orgId: string, userId: string) { roleId: userOrgs.roleId, roleName: roles.name, isOwner: userOrgs.isOwner, - isAdmin: roles.isAdmin + isAdmin: roles.isAdmin, + twoFactorEnabled: users.twoFactorEnabled, }) .from(userOrgs) .leftJoin(roles, eq(userOrgs.roleId, roles.roleId)) diff --git a/server/routers/user/index.ts b/server/routers/user/index.ts index 49278c14..ed8a1769 100644 --- a/server/routers/user/index.ts +++ b/server/routers/user/index.ts @@ -10,3 +10,4 @@ export * from "./adminRemoveUser"; export * from "./listInvitations"; export * from "./removeInvitation"; export * from "./createOrgUser"; +export * from "./updateUser2FA"; diff --git a/server/routers/user/listUsers.ts b/server/routers/user/listUsers.ts index 2e23f401..83c1e492 100644 --- a/server/routers/user/listUsers.ts +++ b/server/routers/user/listUsers.ts @@ -49,7 +49,8 @@ async function queryUsers(orgId: string, limit: number, offset: number) { roleName: roles.name, isOwner: userOrgs.isOwner, idpName: idp.name, - idpId: users.idpId + idpId: users.idpId, + twoFactorEnabled: users.twoFactorEnabled, }) .from(users) .leftJoin(userOrgs, eq(users.userId, userOrgs.userId)) diff --git a/server/routers/user/updateUser2FA.ts b/server/routers/user/updateUser2FA.ts new file mode 100644 index 00000000..845eaa0c --- /dev/null +++ b/server/routers/user/updateUser2FA.ts @@ -0,0 +1,151 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { users, userOrgs } from "@server/db"; +import { eq, and } 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 { ActionsEnum, checkUserActionPermission } from "@server/auth/actions"; +import { OpenAPITags, registry } from "@server/openApi"; + +const updateUser2FAParamsSchema = z + .object({ + userId: z.string(), + orgId: z.string() + }) + .strict(); + +const updateUser2FABodySchema = z + .object({ + twoFactorEnabled: z.boolean() + }) + .strict(); + +export type UpdateUser2FAResponse = { + userId: string; + twoFactorEnabled: boolean; +}; + +registry.registerPath({ + method: "patch", + path: "/org/{orgId}/user/{userId}/2fa", + description: "Update a user's 2FA status within an organization.", + tags: [OpenAPITags.Org, OpenAPITags.User], + request: { + params: updateUser2FAParamsSchema, + body: { + content: { + "application/json": { + schema: updateUser2FABodySchema + } + } + } + }, + responses: {} +}); + +export async function updateUser2FA( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = updateUser2FAParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedBody = updateUser2FABodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { userId, orgId } = parsedParams.data; + const { twoFactorEnabled } = parsedBody.data; + + if (!req.userOrg) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "You do not have access to this organization" + ) + ); + } + + // Check if user has permission to update other users' 2FA + const hasPermission = await checkUserActionPermission( + ActionsEnum.getOrgUser, + req + ); + if (!hasPermission) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "User does not have permission to update other users' 2FA settings" + ) + ); + } + + // Verify the user exists in the organization + const existingUser = await db + .select() + .from(userOrgs) + .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))) + .limit(1); + + if (existingUser.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "User not found or does not belong to the specified organization" + ) + ); + } + + // Update the user's 2FA status + const updatedUser = await db + .update(users) + .set({ + twoFactorEnabled, + // If disabling 2FA, also clear the secret + twoFactorSecret: twoFactorEnabled ? undefined : null + }) + .where(eq(users.userId, userId)) + .returning({ userId: users.userId, twoFactorEnabled: users.twoFactorEnabled }); + + if (updatedUser.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "User not found" + ) + ); + } + + return response(res, { + data: updatedUser[0], + success: true, + error: false, + message: `2FA ${twoFactorEnabled ? 'enabled' : 'disabled'} for user successfully`, + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} \ No newline at end of file diff --git a/src/app/[orgId]/settings/access/users/UsersTable.tsx b/src/app/[orgId]/settings/access/users/UsersTable.tsx index d3ee404e..017ab875 100644 --- a/src/app/[orgId]/settings/access/users/UsersTable.tsx +++ b/src/app/[orgId]/settings/access/users/UsersTable.tsx @@ -34,6 +34,7 @@ export type UserRow = { status: string; role: string; isOwner: boolean; + isTwoFactorEnabled: boolean; }; type UsersTableProps = { @@ -170,6 +171,39 @@ export default function UsersTable({ users: u }: UsersTableProps) { ); } }, + { + accessorKey: "isTwoFactorEnabled", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const userRow = row.original; + + return ( +
+ {userRow.isTwoFactorEnabled && ( + + {t('enabled')} + + ) || ( + + {t('disabled')} + + )} +
+ ); + } + }, { id: "actions", cell: ({ row }) => { diff --git a/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx b/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx index 82999ad2..2929f75b 100644 --- a/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx +++ b/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx @@ -27,6 +27,7 @@ import { ListRolesResponse } from "@server/routers/role"; import { userOrgUserContext } from "@app/hooks/useOrgUserContext"; import { useParams } from "next/navigation"; import { Button } from "@app/components/ui/button"; +import { Checkbox } from "@app/components/ui/checkbox"; import { SettingsContainer, SettingsSection, @@ -43,14 +44,14 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { useTranslations } from "next-intl"; export default function AccessControlsPage() { - const { orgUser: user } = userOrgUserContext(); - + const { orgUser: user, updateOrgUser } = userOrgUserContext(); const api = createApiClient(useEnvContext()); const { orgId } = useParams(); const [loading, setLoading] = useState(false); const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]); + const [enable2FA, setEnable2FA] = useState(user.twoFactorEnabled || false); const t = useTranslations(); @@ -96,7 +97,8 @@ export default function AccessControlsPage() { async function onSubmit(values: z.infer) { setLoading(true); - const res = await api + // Update user role + const roleRes = await api .post< AxiosResponse >(`/role/${values.roleId}/add/${user.userId}`) @@ -109,9 +111,34 @@ export default function AccessControlsPage() { t('accessRoleErrorAddDescription') ) }); + return null; }); - if (res && res.status === 200) { + // Update 2FA status if it changed + if (enable2FA !== user.twoFactorEnabled) { + const twoFARes = await api + .patch(`/org/${orgId}/user/${user.userId}/2fa`, { + twoFactorEnabled: enable2FA + }) + .catch((e) => { + toast({ + variant: "destructive", + title: "Error updating 2FA", + description: formatAxiosError( + e, + "Failed to update 2FA status" + ) + }); + return null; + }); + + if (twoFARes && twoFARes.status === 200) { + // Update the user context with the new 2FA status + updateOrgUser({ twoFactorEnabled: enable2FA }); + } + } + + if (roleRes && roleRes.status === 200) { toast({ variant: "default", title: t('userSaved'), @@ -170,6 +197,36 @@ export default function AccessControlsPage() { )} /> + +
+
+ + setEnable2FA( + e as boolean + ) + } + /> + +
+

+ When enabled, the user will be required to set up their authenticator app on their next login. + {user.twoFactorEnabled && ( + This user currently has 2FA enabled. + )} +

+
+ + @@ -186,6 +243,8 @@ export default function AccessControlsPage() { + + ); } diff --git a/src/app/[orgId]/settings/access/users/page.tsx b/src/app/[orgId]/settings/access/users/page.tsx index 27b227fa..49637f71 100644 --- a/src/app/[orgId]/settings/access/users/page.tsx +++ b/src/app/[orgId]/settings/access/users/page.tsx @@ -81,7 +81,8 @@ export default async function UsersPage(props: UsersPageProps) { idpName: user.idpName || t('idpNameInternal'), status: t('userConfirmed'), role: user.isOwner ? t('accessRoleOwner') : user.roleName || t('accessRoleMember'), - isOwner: user.isOwner || false + isOwner: user.isOwner || false, + isTwoFactorEnabled: user.twoFactorEnabled || false, }; }); diff --git a/src/app/auth/2fa/setup/page.tsx b/src/app/auth/2fa/setup/page.tsx new file mode 100644 index 00000000..2f77aaa5 --- /dev/null +++ b/src/app/auth/2fa/setup/page.tsx @@ -0,0 +1,289 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { AlertCircle, CheckCircle2 } from "lucide-react"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { AxiosResponse } from "axios"; +import { + RequestTotpSecretResponse, + VerifyTotpResponse +} from "@server/routers/auth"; +import { z } from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle +} from "@/components/ui/card"; +import { toast } from "@app/hooks/useToast"; +import { formatAxiosError } from "@app/lib/api"; +import CopyTextBox from "@app/components/CopyTextBox"; +import { QRCodeCanvas } from "qrcode.react"; +import { useTranslations } from "next-intl"; + +export default function Setup2FAPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const redirect = searchParams?.get("redirect"); + const email = searchParams?.get("email"); + + const [step, setStep] = useState(1); + const [secretKey, setSecretKey] = useState(""); + const [secretUri, setSecretUri] = useState(""); + const [loading, setLoading] = useState(false); + const [backupCodes, setBackupCodes] = useState([]); + + const api = createApiClient(useEnvContext()); + const t = useTranslations(); + + // Redirect to login if no email is provided + useEffect(() => { + if (!email) { + router.push('/auth/login'); + } + }, [email, router]); + + const enableSchema = z.object({ + password: z.string().min(1, { message: t('passwordRequired') }) + }); + + const confirmSchema = z.object({ + code: z.string().length(6, { message: t('pincodeInvalid') }) + }); + + const enableForm = useForm>({ + resolver: zodResolver(enableSchema), + defaultValues: { + password: "" + } + }); + + const confirmForm = useForm>({ + resolver: zodResolver(confirmSchema), + defaultValues: { + code: "" + } + }); + + const request2fa = async (values: z.infer) => { + if (!email) return; + + setLoading(true); + + const res = await api + .post>( + `/auth/2fa/setup`, + { + email: email, + password: values.password + } + ) + .catch((e) => { + toast({ + title: t('otpErrorEnable'), + description: formatAxiosError( + e, + t('otpErrorEnableDescription') + ), + variant: "destructive" + }); + }); + + if (res && res.data.data.secret) { + setSecretKey(res.data.data.secret); + setSecretUri(res.data.data.uri); + setStep(2); + } + + setLoading(false); + }; + + const confirm2fa = async (values: z.infer) => { + if (!email) return; + + setLoading(true); + + const { password } = enableForm.getValues(); + + const res = await api + .post>(`/auth/2fa/complete-setup`, { + email: email, + password: password, + code: values.code + }) + .catch((e) => { + toast({ + title: t('otpErrorEnable'), + description: formatAxiosError( + e, + t('otpErrorEnableDescription') + ), + variant: "destructive" + }); + }); + + if (res && res.data.data.valid) { + setBackupCodes(res.data.data.backupCodes || []); + setStep(3); + } + + setLoading(false); + }; + + const handleComplete = () => { + if (redirect) { + router.push(redirect); + } else { + router.push("/"); + } + }; + + return ( +
+ + + {t('otpSetup')} + + Your administrator has enabled two-factor authentication for {email}. + Please complete the setup process to continue. + + + + {step === 1 && ( +
+ +
+ ( + + {t('password')} + + + + + + )} + /> +
+ +
+ + )} + + {step === 2 && ( +
+

+ {t('otpSetupScanQr')} +

+
+ +
+
+ + +
+ +
+ + ( + + + {t('otpSetupSecretCode')} + + + + + + + )} + /> + + + +
+ )} + + {step === 3 && ( +
+ +

+ {t('otpSetupSuccess')} +

+

+ {t('otpSetupSuccessStoreBackupCodes')} +

+ + {backupCodes.length > 0 && ( +
+ + +
+ )} + + +
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/LocaleSwitcher.tsx b/src/components/LocaleSwitcher.tsx index 080c8f7b..fe1dece5 100644 --- a/src/components/LocaleSwitcher.tsx +++ b/src/components/LocaleSwitcher.tsx @@ -48,6 +48,10 @@ export default function LocaleSwitcher() { { value: 'zh-CN', label: '简体中文' + }, + { + value: 'ko-KR', + label: '한국어' } ]} /> diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx index c2fa43cf..04ed25fb 100644 --- a/src/components/LoginForm.tsx +++ b/src/components/LoginForm.tsx @@ -210,6 +210,12 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { return; } + if (data?.twoFactorSetupRequired) { + const setupUrl = `/auth/2fa/setup?email=${encodeURIComponent(email)}${redirect ? `&redirect=${encodeURIComponent(redirect)}` : ''}`; + router.push(setupUrl); + return; + } + if (onLogin) { await onLogin(); } diff --git a/src/i18n/config.ts b/src/i18n/config.ts index 305d66d3..9871a199 100644 --- a/src/i18n/config.ts +++ b/src/i18n/config.ts @@ -1,4 +1,4 @@ export type Locale = (typeof locales)[number]; -export const locales = ['en-US', 'es-ES', 'fr-FR', 'de-DE', 'nl-NL', 'it-IT', 'pl-PL', 'pt-PT', 'tr-TR', 'zh-CN'] as const; +export const locales = ['en-US', 'es-ES', 'fr-FR', 'de-DE', 'nl-NL', 'it-IT', 'pl-PL', 'pt-PT', 'tr-TR', 'zh-CN', 'ko-KR'] as const; export const defaultLocale: Locale = 'en-US'; \ No newline at end of file