diff --git a/.env.example b/.env.example index 07e46c3e..3c4ba592 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ AUTH_MAX_AGE="2592000" AWS_CLOUDFRONT_DOMAIN= + AWS_S3_ACCESS_KEY= AWS_S3_BUCKET="nuxt-template" AWS_S3_REGION= @@ -30,15 +31,16 @@ GOOGLE_CLIENT_SECRET= LOGTO_APP_ID= LOGTO_APP_SECRET= LOGTO_COOKIE_ENCRYPTION_KEY= -LOGTO_ENDPOINT="" +LOGTO_ENDPOINT="https://auth.thecodeorigin.com" LOGTO_ADMIN_APP_ID="" LOGTO_ADMIN_APP_SECRET="" MONGODB_COLLECTION_NAME="nuxt-template-cache" -MONGODB_CONNECTION_STRING= +MONGODB_CONNECTION_STRING="mongodb://root:password@localhost:27017/nuxt-template?authSource=admin" MONGODB_DATABASE_NAME="nuxt-template" +NUXT_APP_CDN_URL="" NUXT_PUBLIC_API_BASE_URL="http://localhost:3000" NUXT_PUBLIC_APP_BASE_URL="http://localhost:3000" NUXT_PUBLIC_APP_NAME="nuxt-template" @@ -52,9 +54,9 @@ POSTGRES_PASSWORD="postgres" POSTGRES_PORT="54321" POSTGRES_USER="postgres" -REDIS_HOST= -REDIS_PASSWORD= -REDIS_PORT= +REDIS_HOST="localhost" +REDIS_PASSWORD="secret" +REDIS_PORT="6379" REDIS_USER="default" SMTP_FROM="noreply@example.com" @@ -70,10 +72,6 @@ STRIPE_PUBLISHABLE_KEY= STRIPE_SECRET_KEY= STRIPE_WEBHOOK_SECRET= -VERCEL_ORG_ID= -VERCEL_PROJECT_ID= -VERCEL_TOKEN= - VNPAY_DISABLE_TEST_MODE= VNPAY_HASHSECRET= VNPAY_TMNCODE= @@ -81,3 +79,8 @@ VNPAY_TMNCODE= PAYOS_CLIENT_ID= PAYOS_API_KEY= PAYOS_CHECKSUM_KEY= + +SEPAY_WEBHOOK_SIGNING_KEY= +SEPAY_BANK_NUMBER= +SEPAY_BANK_NAME= +SEPAY_TRANSACTION_PREFIX= diff --git a/.gitignore b/.gitignore index 843c876d..7d9e3dfa 100644 --- a/.gitignore +++ b/.gitignore @@ -53,12 +53,10 @@ public/mockServiceWorker.js # Ignore the build directory public/firebase-config.json -docker/postgres -docker/redis - server/db/schemas/cjs server/db/schemas/mjs server/db/migrations areas +docker diff --git a/.stylelintrc.json b/.stylelintrc.json index 815fe5d8..6bf62ca7 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,11 +1,7 @@ { "extends": [ "stylelint-config-standard-scss", - "stylelint-config-idiomatic-order", - "@stylistic/stylelint-config" - ], - "plugins": [ - "@stylistic/stylelint-plugin" + "stylelint-config-idiomatic-order" ], "overrides": [ { @@ -22,13 +18,6 @@ } ], "rules": { - "@stylistic/max-line-length": [ - 220, - { - "ignore": "comments" - } - ], - "@stylistic/indentation": 2, "selector-class-pattern": null, "color-function-notation": null, "annotation-no-unknown": [ diff --git a/Dockerfile b/Dockerfile index f8944c47..96f2c7c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,42 +1,47 @@ -# Step 1: Base image for building -FROM node:22-bullseye-slim AS build -# Step 2: Set working directory +# Dockerfile +# Production-grade Dockerfile for Nuxt 3 with PNPM and Node 22 Alpine, multi-stage build +# --- Stage 1: Base Image Setup --- +FROM node:22-alpine AS base + +# Set working directory WORKDIR /app -# Step 3: Install dependencies for pnpm -RUN npm install -g pnpm@10.2.1 # Ensure a specific version of pnpm -# Step 4: Copy application files + +# --- Stage 2: Dependencies Installation --- +FROM base AS builder + +# Install pnpm globally +RUN npm install -g pnpm@latest + +COPY scripts/ ./scripts COPY .npmrc ./ COPY .nuxtignore ./ COPY package*.json ./ COPY pnpm-*.yaml ./ COPY *.config.ts ./ COPY tsconfig.json ./ -COPY package.json package.json +COPY public/ ./public COPY . . -# Step 5: Clean up old node_modules (if any) and install dependencies -RUN rm -rf node_modules && pnpm install -# Step 6: Build the application -RUN pnpm build -# Step 7: Use a smaller image for production -FROM node:22-bullseye-slim AS prod -# Step 8: Set working directory -WORKDIR /app -# Step 9: Install required packages (curl needed for AWS CLI) -RUN apt-get update && apt-get install -y curl unzip -# Step 10: Install pnpm (again) to ensure it's available in the production environment -RUN npm install -g pnpm@10.2.1 # Ensure the same version of pnpm -# Step 11: Copy built application from build stage -COPY --from=build /app /app -# Step 12: Remove unnecessary dev dependencies + +# Install dependencies RUN pnpm install -# Step 13: Install AWS CLI -RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" \ - && unzip awscliv2.zip \ - && ./aws/install \ - && rm -rf awscliv2.zip aws -# Step 14: Expose port 3000 +RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm build + +FROM node:22-alpine AS production + +# Set working directory +WORKDIR /app + +RUN apk add --no-cache curl + +# Copy the .output directory from the builder stage +COPY --from=builder /app/.output ./.output + +COPY --from=builder /app/.env ./.env + +ENV NITRO_HOST=0.0.0.0 +ENV NITRO_PORT=3000 + EXPOSE 3000 -# Step 15: Push output files to S3 before starting the application -CMD aws s3 sync /app/.output s3://$AWS_S3_BUCKET -# Step 16: Start the application -CMD ["pnpm", "start"] + +# Command to run the Nuxt application +CMD ["node", ".output/server/index.mjs"] diff --git a/app/@core/scss/base/skins/_bordered.scss b/app/@core/scss/base/skins/_bordered.scss index ade00d92..7a8fa715 100644 --- a/app/@core/scss/base/skins/_bordered.scss +++ b/app/@core/scss/base/skins/_bordered.scss @@ -25,9 +25,7 @@ $header: ".layout-navbar"; .layout-nav-type-vertical.window-scrolled #{$header} { border-block-start: none !important; } - } - // stylelint-disable-next-line @stylistic/indentation - @else { + } @else { @include mixins.bordered-skin(".layout-nav-type-vertical.window-scrolled.layout-navbar-sticky #{$header}", "border-bottom"); } diff --git a/app/@layouts/components/NavBarNotifications.vue b/app/@layouts/components/NavBarNotifications.vue index 2f4543b2..947f325f 100644 --- a/app/@layouts/components/NavBarNotifications.vue +++ b/app/@layouts/components/NavBarNotifications.vue @@ -1,9 +1,10 @@ - - diff --git a/app/@layouts/components/UserProfile.vue b/app/@layouts/components/UserProfile.vue index 7be2828f..8c5c804b 100644 --- a/app/@layouts/components/UserProfile.vue +++ b/app/@layouts/components/UserProfile.vue @@ -4,18 +4,16 @@ import { PerfectScrollbar } from 'vue3-perfect-scrollbar' const { t } = useI18n() const authStore = useAuthStore() -const tokenDeviceStore = useTokenDeviceStore() -const userEmail = computed(() => authStore.currentUser?.email) -const userAvatar = computed(() => authStore.currentUser?.picture) +const userEmail = computed(() => authStore.currentUser?.primary_email) +const userAvatar = computed(() => authStore.currentUser?.avatar) const userFullname = computed(() => authStore.currentUser?.name) -const userRole = computed(() => authStore.currentUser?.roles?.[0] || authStore.currentUser?.organization_roles?.[0] || t('User')) async function logout() { try { - if (authStore.currentUser) - await tokenDeviceStore.clearTokenDevice() + const referCode = useCookie('referCode') + referCode.value = null - await authStore.signOut() + await navigateTo({ path: '/sign-out' }, { external: true }) navigateTo({ name: 'auth-login' }) } @@ -51,12 +49,12 @@ const userProfileList = computed @@ -114,8 +112,8 @@ const userProfileList = computed {{ userFullname || userEmail }} -
- {{ userRole }} +
+ {{ userEmail }}
diff --git a/app/@layouts/components/layout/DefaultLayoutWithHorizontalNav.vue b/app/@layouts/components/layout/DefaultLayoutWithHorizontalNav.vue index ecd89e3d..47c9ede1 100644 --- a/app/@layouts/components/layout/DefaultLayoutWithHorizontalNav.vue +++ b/app/@layouts/components/layout/DefaultLayoutWithHorizontalNav.vue @@ -30,8 +30,7 @@ const config = useRuntimeConfig() - - + diff --git a/app/@layouts/components/layout/DefaultLayoutWithVerticalNav.vue b/app/@layouts/components/layout/DefaultLayoutWithVerticalNav.vue index 4e128fe1..4815db42 100644 --- a/app/@layouts/components/layout/DefaultLayoutWithVerticalNav.vue +++ b/app/@layouts/components/layout/DefaultLayoutWithVerticalNav.vue @@ -39,7 +39,6 @@ watch([ - diff --git a/app/@layouts/components/navigation/HorizontalNavLink.vue b/app/@layouts/components/navigation/HorizontalNavLink.vue index f5e83704..f9caca98 100644 --- a/app/@layouts/components/navigation/HorizontalNavLink.vue +++ b/app/@layouts/components/navigation/HorizontalNavLink.vue @@ -20,10 +20,14 @@ const { can } = useAbility() const router = useRouter() const visible = computed(() => { - if (!props.item.action || !props.item.subject) + if (!props.item.scopes) return true - return can(props.item.action, props.item.subject) + return props.item.scopes.some((scope: string) => { + const [action, subject] = scope.split(':') as [string, string] + + return can(action, subject) + }) }) diff --git a/app/@layouts/components/navigation/VerticalNavLink.vue b/app/@layouts/components/navigation/VerticalNavLink.vue index 0542e6cc..846ce2ba 100644 --- a/app/@layouts/components/navigation/VerticalNavLink.vue +++ b/app/@layouts/components/navigation/VerticalNavLink.vue @@ -17,10 +17,14 @@ const { can } = useAbility() const router = useRouter() const visible = computed(() => { - if (!props.item.action || !props.item.subject) + if (!props.item.scopes) return true - return can(props.item.action, props.item.subject) + return props.item.scopes.some((scope: string) => { + const [action, subject] = scope.split(':') as [string, string] + + return can(action, subject) + }) }) diff --git a/app/@layouts/components/navigation/VerticalNavSectionTitle.vue b/app/@layouts/components/navigation/VerticalNavSectionTitle.vue index b1389bf3..c75e25e1 100644 --- a/app/@layouts/components/navigation/VerticalNavSectionTitle.vue +++ b/app/@layouts/components/navigation/VerticalNavSectionTitle.vue @@ -14,10 +14,14 @@ const shallRenderIcon = configStore.isVerticalNavMini() const { can } = useAbility() const visible = computed(() => { - if (!props.item.action || !props.item.subject) + if (!props.item.scopes) return true - return can(props.item.action, props.item.subject) + return props.item.scopes.some((scope: string) => { + const [action, subject] = scope.split(':') as [string, string] + + return can(action, subject) + }) }) diff --git a/app/@layouts/plugins/casl.ts b/app/@layouts/plugins/casl.ts index 6149e543..50d18a79 100644 --- a/app/@layouts/plugins/casl.ts +++ b/app/@layouts/plugins/casl.ts @@ -8,8 +8,12 @@ export function canNavigate(to: RouteLocationNormalized) { const { can } = useAbility() - if (!to.meta.action || !to.meta.subject) - return true + return !to.meta.scopes || ( + Array.isArray(to.meta.scopes) + && to.meta.scopes.some((scope: string) => { + const [action, subject] = scope.split(':') as [string, string] - return can(to.meta.action, to.meta.subject) + return can(action, subject) + }) + ) } diff --git a/app/api/auth.ts b/app/api/auth.ts new file mode 100644 index 00000000..bb58fa78 --- /dev/null +++ b/app/api/auth.ts @@ -0,0 +1,28 @@ +import type { User } from '@base/server/types/models' + +export function useApiAuth() { + function fetchProfile() { + // Get user credit from our database instead of depending on Logto data + return $api<{ data: User }>('/api/auth/me') + } + + function updateProfile(payload: Partial<{ username: string, name: string, avatar: string }>) { + return $api('/api/auth/me', { + method: 'PATCH', + body: payload, + }) + } + + function updatePassword(payload: Partial<{ password: string }>) { + return $api('/api/auth/password', { + method: 'POST', + body: payload, + }) + } + + return { + fetchProfile, + updateProfile, + updatePassword, + } +} diff --git a/app/api/casl.ts b/app/api/casl.ts new file mode 100644 index 00000000..6a0eae10 --- /dev/null +++ b/app/api/casl.ts @@ -0,0 +1,9 @@ +export function useApiCasl() { + function fetchScopes() { + return $api('/api/scopes') + } + + return { + fetchScopes, + } +} diff --git a/app/api/credit.ts b/app/api/credit.ts new file mode 100644 index 00000000..20ccaee2 --- /dev/null +++ b/app/api/credit.ts @@ -0,0 +1,12 @@ +import type { User } from '@base/server/types/models' + +export function useApiCredit() { + function fetchCredit() { + // Get user credit from our database instead of depending on Logto data + return $api<{ data: User }>('/api/auth/me') + } + + return { + fetchCredit, + } +} diff --git a/app/api/health.ts b/app/api/health.ts new file mode 100644 index 00000000..97ff6d67 --- /dev/null +++ b/app/api/health.ts @@ -0,0 +1,9 @@ +export function useApiHealth() { + function fetchHealthCheck() { + return $api<{ success: true }>('/api/health') + } + + return { + fetchHealthCheck, + } +} diff --git a/app/api/notification.ts b/app/api/notification.ts new file mode 100644 index 00000000..3519d11a --- /dev/null +++ b/app/api/notification.ts @@ -0,0 +1,89 @@ +import type { ParsedFilterQuery } from '@base/server/utils/filter' + +export function useApiNotification() { + function fetchNotifications(query?: Partial) { + return $api(`/api/notifications`, { + query, + }) + } + + function markRead(id: string) { + return $api(`/api/notifications/${id}/read`, { + method: 'PATCH', + body: { + read_at: new Date(), + }, + }) + } + + function markUnread(id: string) { + return $api(`/api/notifications/${id}/unread`, { + method: 'PATCH', + body: { + read_at: null, + }, + }) + } + + function markAllRead() { + return $api(`/api/notifications/read`, { + method: 'PATCH', + }) + } + + function markAllUnread() { + return $api(`/api/notifications/unread`, { + method: 'PATCH', + }) + } + + function deleteNotification(id: string) { + return $api(`/api/notifications/${id}`, { + method: 'DELETE', + }) + } + + function countUnreadNotifications() { + return $api(`/api/notifications/unread`) + } + + function createTokenDevice(token: string) { + return $api(`/api/devices`, { + method: 'POST', + body: { token }, + }) + } + + function deleteTokenDevice(token: string) { + return $api(`/api/devices`, { + method: 'DELETE', + body: { token }, + }) + } + + function updateSettings(payload: Partial<{ + email: boolean | null + desktop: boolean | null + product_updates: boolean | null + weekly_digest: boolean | null + important_updates: boolean | null + }>) { + return $api('/api/auth/notification', { + method: 'PATCH', + body: payload, + }) + } + + return { + fetchNotifications, + markRead, + markUnread, + markAllRead, + markAllUnread, + deleteNotification, + countUnreadNotifications, + createTokenDevice, + deleteTokenDevice, + updateSettings, + } +} diff --git a/app/api/payment.ts b/app/api/payment.ts new file mode 100644 index 00000000..3de49948 --- /dev/null +++ b/app/api/payment.ts @@ -0,0 +1,37 @@ +import type { PaymentStatus } from '@base/server/db/schemas' + +export function useApiPayment() { + function checkout(type: 'payos' | 'vnpay' | 'sepay', productIdentifier: string) { + if (type !== 'payos' && type !== 'vnpay' && type !== 'sepay') + throw new Error('Invalid payment provider') + + return $api<{ + data: { + message: string + paymentUrl: string + } + }>(`api/payments/${type}/checkout`, { + method: 'POST', + body: { + productIdentifier, + }, + }) + } + + function checkStatus(type: 'payos' | 'vnpay' | 'sepay', description: string) { + return $api<{ + data: { + status: PaymentStatus + } + }>(`api/payments/${type}/status`, { + params: { + description, + }, + }) + } + + return { + checkout, + checkStatus, + } +} diff --git a/app/api/product.ts b/app/api/product.ts new file mode 100644 index 00000000..1b3f594e --- /dev/null +++ b/app/api/product.ts @@ -0,0 +1,19 @@ +import type { productTable } from '@base/server/db/schemas' +import type { InferSelectModel } from 'drizzle-orm' + +export type Product = InferSelectModel + +export function useApiProduct() { + function fetchProducts() { + return $api<{ data: Product[] }>('/api/products') + } + + function fetchCreditPackages() { + return $api<{ data: Product[] }>('/api/products/credit-packages') + } + + return { + fetchProducts, + fetchCreditPackages, + } +} diff --git a/app/api/ref.ts b/app/api/ref.ts new file mode 100644 index 00000000..83f94e33 --- /dev/null +++ b/app/api/ref.ts @@ -0,0 +1,28 @@ +import type { ParsedFilterQuery } from '@base/server/utils/filter' + +export function useApiReference() { + function fetchAvailableReferences() { + return $api(`/api/ref/available`, { + method: 'GET', + }) + } + + function fetchUsageHistoryReferences(query?: Partial) { + return $api(`/api/ref/history`, { + method: 'GET', + query, + }) + } + + function fetchReferenceByCode(referCode: string) { + return $api(`/api/ref/${referCode}`, { + method: 'GET' + }) + } + + return { + fetchAvailableReferences, + fetchUsageHistoryReferences, + fetchReferenceByCode + } +} diff --git a/app/stores/s3.ts b/app/api/s3.ts similarity index 57% rename from app/stores/s3.ts rename to app/api/s3.ts index 044204ef..f2325c12 100644 --- a/app/stores/s3.ts +++ b/app/api/s3.ts @@ -1,6 +1,6 @@ -export const useS3Store = defineStore('s3', () => { +export function useApiS3() { function getSignedUrl(filename: string) { - return $api('/api/s3', { + return $api<{ uploadUrl: string, assetUrl: string }>('/api/s3', { method: 'PUT', body: { filename }, }) @@ -9,4 +9,4 @@ export const useS3Store = defineStore('s3', () => { return { getSignedUrl, } -}) +} diff --git a/app/api/stripe.ts b/app/api/stripe.ts new file mode 100644 index 00000000..40354dad --- /dev/null +++ b/app/api/stripe.ts @@ -0,0 +1,30 @@ +export function useApiStripe() { + function fetchStripeProducts() { + return $api('/api/payments/stripe/products') + } + + function fetchStripePrices(productId: string) { + return $api(`/api/payments/stripe/products/${productId}/prices`) + } + + function fetchStripeSubscription() { + return $api('/api/payments/stripe/me') + } + + async function createSubscriptionCheckoutUrl(customerId: string, priceId: string) { + return $api<{ url: string }>(`/api/payments/stripe/customers/${customerId}/checkout`, { + method: 'POST', + body: { + priceId, + redirectPath: '/settings/billing-plans', + }, + }) + } + + return { + fetchStripeProducts, + fetchStripePrices, + fetchStripeSubscription, + createSubscriptionCheckoutUrl, + } +} diff --git a/app/app.vue b/app/app.vue index 5b1676d0..d425abcd 100644 --- a/app/app.vue +++ b/app/app.vue @@ -1,16 +1,15 @@ diff --git a/app/assets/locale/en.json b/app/assets/locale/en.json index 5be0cd20..7dcd567e 100644 --- a/app/assets/locale/en.json +++ b/app/assets/locale/en.json @@ -137,5 +137,25 @@ "Users": "Users", "Are you sure you want to delete this shortcut?": "Are you sure you want to delete this shortcut?", "Account settings updated successfully": "Account settings updated successfully", - "Stripe customer self-service portal is not currently available!": "Stripe customer self-service portal is not currently available!" + "Stripe customer self-service portal is not currently available!": "Stripe customer self-service portal is not currently available!", + "credits": "credits", + "Payment Information": "Payment Information", + "Please manage your payment information to ensure your service is not interrupted.": "Please manage your payment information to ensure your service is not interrupted.", + "Credits available": "Credits available:", + "We will notify you if your credit is running low.": "We will notify you if your credit is running low.", + "Topup Credits": "Topup Credits", + "Select a credit package": "Select a credit package", + "Topup Now": "Topup Now", + "Payment successful": "Payment successful", + "We have not received your payment yet. Please try again later, or contact support if the issue persists.": "We have not received your payment yet. Please try again later, or contact support if the issue persists.", + "Bank": "Bank", + "Account Number": "Account Number", + "Amount": "Amount", + "Description": "Description", + "Please scan the QR code or transfer the amount to the bank account to complete the payment.": "Please scan the QR code or transfer the amount to the bank account to complete the payment.", + "Confirm Payment": "Confirm Payment", + "Available referral code links": "Available referral code links", + "No reference links available": "No reference links available", + "Account information": "Account information", + "Reference history": "Reference History" } diff --git a/app/assets/locale/vi.json b/app/assets/locale/vi.json index f2d6da23..0efe32fa 100644 --- a/app/assets/locale/vi.json +++ b/app/assets/locale/vi.json @@ -137,5 +137,25 @@ "Users": "Người dùng", "Are you sure you want to delete this shortcut?": "Bạn có chắc chắn muốn xóa phím tắt này không?", "Account settings updated successfully": "Thông tin tài khoản đã được cập nhật thành công", - "Stripe customer self-service portal is not currently available!": "Cổng tự phục vụ của thanh toán điện tử Stripe hiện không khả dụng!" + "Stripe customer self-service portal is not currently available!": "Cổng tự phục vụ khách hàng của Stripe hiện không khả dụng!", + "credits": "Tín dụng", + "Payment Information": "Thông tin thanh toán", + "Please manage your payment information to ensure your service is not interrupted.": "Vui lòng quản lý thông tin thanh toán của bạn để đảm bảo dịch vụ không bị gián đoạn.", + "Credits available": "Tín dụng khả dụng", + "We will notify you if your credit is running low.": "Chúng tôi sẽ thông báo nếu tín dụng của bạn sắp hết.", + "Topup Credits": "Nạp thêm tín dụng", + "Select a credit package": "Chọn gói tín dụng", + "Topup Now": "Nạp ngay", + "Payment successful": "Thanh toán thành công", + "We have not received your payment yet. Please try again later, or contact support if the issue persists.": "Chúng tôi chưa nhận được thanh toán của bạn. Vui lòng thử lại sau hoặc liên hệ hỗ trợ nếu vấn đề vẫn tiếp diễn.", + "Bank": "Ngân hàng", + "Account Number": "Số tài khoản", + "Amount": "Số tiền", + "Description": "Mô tả", + "Please scan the QR code or transfer the amount to the bank account to complete the payment.": "Vui lòng quét mã QR hoặc chuyển khoản số tiền vào tài khoản ngân hàng để hoàn tất thanh toán.", + "Confirm Payment": "Xác nhận thanh toán", + "Available referral code links": "Liên kết mã giới thiệu khả dụng", + "No reference links available": "Không có liên kết giới thiệu nào khả dụng", + "Account information": "Thông tin tài khoản", + "Reference history": "Lịch sử giới thiệu" } diff --git a/app/components/account-settings/AccountSettingsAccount.vue b/app/components/account-settings/AccountSettingsAccount.vue index f5dde848..658e9e63 100644 --- a/app/components/account-settings/AccountSettingsAccount.vue +++ b/app/components/account-settings/AccountSettingsAccount.vue @@ -1,41 +1,68 @@ - diff --git a/app/components/account-settings/AccountSettingsCredit.vue b/app/components/account-settings/AccountSettingsCredit.vue new file mode 100644 index 00000000..275aaf82 --- /dev/null +++ b/app/components/account-settings/AccountSettingsCredit.vue @@ -0,0 +1,108 @@ + + + diff --git a/app/composables/useMessaging.ts b/app/composables/useMessaging.ts deleted file mode 100644 index 7356cde3..00000000 --- a/app/composables/useMessaging.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { getMessaging } from 'firebase/messaging' -import { useFirebaseApp } from 'vuefire' - -export function useMessaging() { - return getMessaging(useFirebaseApp()) -} diff --git a/app/error.vue b/app/error.vue index e9a77c92..7eccd4c1 100644 --- a/app/error.vue +++ b/app/error.vue @@ -12,7 +12,7 @@ defineProps<{ diff --git a/app/middleware/01.health.global.ts b/app/middleware/01.health.global.ts index b2351c95..4bcf4454 100644 --- a/app/middleware/01.health.global.ts +++ b/app/middleware/01.health.global.ts @@ -2,10 +2,8 @@ export default defineNuxtRouteMiddleware(async (to) => { if (to.meta.public || import.meta.prerender) return - const healthStore = useHealthStore() - try { - await healthStore.fetchHealthCheck() + await useApiHealth().fetchHealthCheck() } catch { throw createError({ diff --git a/app/middleware/02.authentication.global.ts b/app/middleware/02.authentication.global.ts index b2645a70..22d1cc7b 100644 --- a/app/middleware/02.authentication.global.ts +++ b/app/middleware/02.authentication.global.ts @@ -2,24 +2,20 @@ export default defineNuxtRouteMiddleware(async (to) => { if (to.meta.public) return - if (to.meta.auth || to.meta.auth === undefined) { - const authStore = useAuthStore() + const authStore = useAuthStore() - if (authStore.currentUser) { - if (to.meta.unauthenticatedOnly) - return navigateTo('/') + try { + await authStore.fetchProfile() + } + catch {} - try { - await authStore.fetchToken() - } - catch { - notifyError({ - content: 'Failed to retrieve user token.', - }) - } - } - else if (!to.meta.unauthenticatedOnly) { - return navigateTo('/auth/login') + if (to.meta.unauthenticatedOnly && authStore.currentUser) { + return navigateTo({ path: '/dashboard' }) + } + + if (to.meta.auth || to.meta.auth === undefined) { + if (!authStore.currentUser) { + return navigateTo({ path: '/auth/login' }) } } }) diff --git a/app/pages/auth/login.vue b/app/pages/auth/login.vue index ac006035..766666eb 100644 --- a/app/pages/auth/login.vue +++ b/app/pages/auth/login.vue @@ -20,6 +20,7 @@ const authThemeMask = useGenerateImageVariant(authV2LoginMaskLight, authV2LoginM definePageMeta({ layout: 'blank', + auth: false, unauthenticatedOnly: true, }) diff --git a/app/pages/checkout.vue b/app/pages/checkout.vue new file mode 100644 index 00000000..6f01993c --- /dev/null +++ b/app/pages/checkout.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/app/pages/dashboard.vue b/app/pages/dashboard.vue index c3b9e6ac..24b0d413 100644 --- a/app/pages/dashboard.vue +++ b/app/pages/dashboard.vue @@ -1,5 +1,6 @@ - - - - diff --git a/app/pages/settings/[tab].vue b/app/pages/settings/[tab].vue index cd8be895..8f5545ad 100644 --- a/app/pages/settings/[tab].vue +++ b/app/pages/settings/[tab].vue @@ -60,7 +60,7 @@ definePageMeta({ - + diff --git a/app/plugins/auth.ts b/app/plugins/auth.ts deleted file mode 100644 index 6f63bacc..00000000 --- a/app/plugins/auth.ts +++ /dev/null @@ -1,17 +0,0 @@ -export default defineNuxtPlugin({ - name: 'auth', - async setup() { - const authStore = useAuthStore() - - if (authStore.currentUser) { - try { - await authStore.fetchToken() - } - catch { - notifyError({ - content: 'Failed to retrieve user token.', - }) - } - } - }, -}) diff --git a/app/plugins/casl.ts b/app/plugins/casl.ts index 34d5e3b6..de94549f 100644 --- a/app/plugins/casl.ts +++ b/app/plugins/casl.ts @@ -1,5 +1,4 @@ export default defineNuxtPlugin({ - dependsOn: ['auth'], async setup() { const caslStore = useCaslStore() diff --git a/app/plugins/notification.client.ts b/app/plugins/notification.client.ts deleted file mode 100644 index 6d11ea8a..00000000 --- a/app/plugins/notification.client.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { getMessaging, getToken } from 'firebase/messaging' -import { isInAppBrowser } from '~/utils/browser' - -export default defineNuxtPlugin({ - setup(nuxtApp) { - const authStore = useAuthStore() - const healthStore = useHealthStore() - const tokenDeviceStore = useTokenDeviceStore() - - const config = useRuntimeConfig() - - nuxtApp.hook('app:mounted', async () => { - if (healthStore.isHealthy && !isInAppBrowser()) { - if (authStore.currentUser) { - try { - if (Notification.permission !== 'granted') - await Notification.requestPermission() - - if (Notification.permission === 'granted' && authStore.currentUser) { - const messaging = getMessaging() - const token = await getToken(messaging, { vapidKey: config.public.firebase.keyPair }) - await tokenDeviceStore.setTokenDevice(token) - } - } - catch {} - } - } - }) - }, -}) diff --git a/app/stores/auth.ts b/app/stores/auth.ts index d5d2982e..3376dec9 100644 --- a/app/stores/auth.ts +++ b/app/stores/auth.ts @@ -1,45 +1,30 @@ -import type { UserInfoResponse } from '@logto/nuxt' +import type { User } from '@base/server/types/models' export const useAuthStore = defineStore('auth', () => { - const config = useRuntimeConfig() + const currentUser = useState('currentUser', () => null) - const client = useLogtoClient() + const crsfToken = computed(() => { + if (import.meta.server) + return useNuxtApp().ssrContext?.event?.context?.csrfToken - const accessToken = useState('accessToken', () => null) + return window.document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') + }) - const currentUser = useLogtoUser() as UserInfoResponse & { - custom_data?: { credit: number } - } | null + const authApi = useApiAuth() - const currentRoles = computed(() => currentUser?.roles || null) + async function fetchProfile() { + if (!currentUser.value) { + const response = await authApi.fetchProfile() - async function fetchToken() { - if (client && !accessToken.value && await client.isAuthenticated()) - accessToken.value = await client.getAccessToken(config.public.apiBaseUrl) - } - - function signIn() { - return navigateTo({ path: '/sign-in' }, { external: true }) - } - - function signOut() { - return navigateTo({ path: '/sign-out' }, { external: true }) - } + currentUser.value = response.data + } - function updateProfile(payload: Partial<{ username: string, name: string, avatar: string }>) { - return $api('/api/me', { - method: 'PATCH', - body: payload, - }) + return currentUser.value } return { - accessToken, + crsfToken, currentUser, - currentRoles, - signIn, - signOut, - fetchToken, - updateProfile, + fetchProfile, } }) diff --git a/app/stores/casl.ts b/app/stores/casl.ts index fca99bd2..95d0f099 100644 --- a/app/stores/casl.ts +++ b/app/stores/casl.ts @@ -38,7 +38,7 @@ export const useCaslStore = defineStore('casl', () => { ) async function fetchScopes() { - const response = await $api('/api/scopes') + const response = await useApiCasl().fetchScopes() if (Array.isArray(response)) { scopes.value = response.map((scope) => { diff --git a/app/stores/faq.ts b/app/stores/faq.ts deleted file mode 100644 index 8eba79dd..00000000 --- a/app/stores/faq.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const useFaqStore = defineStore('faq', () => { - async function fetchFaqs(query: Partial) { - return $api('/api/faq', { - query, - }) - } - - return { - fetchFaqs, - } -}) diff --git a/app/stores/health.ts b/app/stores/health.ts deleted file mode 100644 index d51dc59d..00000000 --- a/app/stores/health.ts +++ /dev/null @@ -1,19 +0,0 @@ -export const useHealthStore = defineStore('health', () => { - const isHealthy = ref(false) - - async function fetchHealthCheck() { - try { - await $api('/api/health') - - isHealthy.value = true - } - catch { - isHealthy.value = false - } - } - - return { - isHealthy, - fetchHealthCheck, - } -}) diff --git a/app/stores/notification.ts b/app/stores/notification.ts deleted file mode 100644 index 219cdca1..00000000 --- a/app/stores/notification.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { ParsedFilterQuery } from '@base/server/utils/filter' - -export const useNotificationStore = defineStore('notification', () => { - const authStore = useAuthStore() - - function fetchNotifications(query: Partial) { - return $api(`/api/users/${authStore.currentUser?.sub}/notifications`, { - query, - }) - } - - async function markRead(id: string) { - return $api(`/api/users/${authStore.currentUser?.sub}/notifications/${id}`, { - method: 'PATCH', - body: { - read_at: new Date(), - }, - }) - } - - async function markUnread(id: string) { - return $api(`/api/users/${authStore.currentUser?.sub}/notifications/${id}`, { - method: 'PATCH', - body: { - read_at: null, - }, - }) - } - - async function markAllRead() { - return $api(`/api/users/${authStore.currentUser?.sub}/notifications/mark-all-read`, { - method: 'PATCH', - }) - } - - async function markAllUnread() { - return $api(`/api/users/${authStore.currentUser?.sub}/notifications/mark-all-unread`, { - method: 'PATCH', - }) - } - - async function deleteNotification(id: string) { - return $api(`/api/users/${authStore.currentUser?.sub}/notifications/${id}`, { - method: 'DELETE', - }) - } - async function countUnreadNotifications() { - return $api(`/api/users/${authStore.currentUser?.sub}/notifications/unread`) - } - return { - fetchNotifications, - markRead, - markUnread, - markAllRead, - markAllUnread, - deleteNotification, - countUnreadNotifications, - } -}) diff --git a/app/stores/payment.ts b/app/stores/payment.ts deleted file mode 100644 index d43b1728..00000000 --- a/app/stores/payment.ts +++ /dev/null @@ -1,24 +0,0 @@ -export const usePaymentStore = defineStore('payment', () => { - async function checkout(type: 'payos' | 'vnpay', productIdentifier: string) { - if (type !== 'payos' && type !== 'vnpay') - throw new Error('Invalid payment provider') - - const { data } = await $api<{ - data: { - message: string - paymentUrl: string - } - }>(`/api/payments/${type}/checkout`, { - method: 'POST', - body: { - productIdentifier, - }, - }) - - return data - } - - return { - checkout, - } -}) diff --git a/app/stores/shortcut.ts b/app/stores/shortcut.ts deleted file mode 100644 index 295fcdf2..00000000 --- a/app/stores/shortcut.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { userShortcutTable } from '@base/server/db/schemas' -import type { InferSelectModel } from 'drizzle-orm' - -export type RawShortcut = InferSelectModel - -export const useShortcutStore = defineStore('shortcut', () => { - const authStore = useAuthStore() - - const userShortcuts = ref([]) - - async function getUserShortcuts() { - if (!userShortcuts.value.length) { - const response = await $api(`/api/users/${authStore.currentUser?.sub}/shortcuts`) - - userShortcuts.value = response.data - } - - return userShortcuts.value - } - - async function postUserShortcut(route: string) { - const response = await $api(`/api/users/${authStore.currentUser?.sub}/shortcuts`, { - method: 'POST', - body: JSON.stringify({ - route, - }), - }) - - userShortcuts.value.push(response.data) - } - - async function deleteUserShortcut(shortcutId: string) { - const response = await $api(`/api/users/${authStore.currentUser?.sub}/shortcuts/${shortcutId}`, { - method: 'DELETE', - }) - - userShortcuts.value = userShortcuts.value.filter(shortcut => shortcut.id !== response.data[0]?.id) - } - return { - userShortcuts, - getUserShortcuts, - postUserShortcut, - deleteUserShortcut, - } -}) diff --git a/app/stores/stripe.ts b/app/stores/stripe.ts index 384e17a9..f5ce0f8e 100644 --- a/app/stores/stripe.ts +++ b/app/stores/stripe.ts @@ -1,21 +1,28 @@ import type Stripe from 'stripe' export const useStripeStore = defineStore('stripe', () => { - const stripeProducts = ref([]) - const stripePrices = ref>({}) + const stripePrices = ref([]) - async function fetchStripeProductPrices() { - stripeProducts.value = await $api('/api/payments/stripe/products') + const stripeApi = useApiStripe() - for (const product of stripeProducts.value) { - stripePrices.value[product.id] = await $api(`/api/payments/stripe/products/${product.id}/prices`) + async function fetchStripeProductPrices() { + if (stripePrices.value && stripePrices.value.length > 0) { + return stripePrices.value } + const { data: products } = await stripeApi.fetchStripeProducts() + + if (!products?.[0]) + return [] + + const res = await stripeApi.fetchStripePrices(products[0].id) + + stripePrices.value = res.data || [] + return stripePrices.value } return { - stripeProducts, stripePrices, fetchStripeProductPrices, } diff --git a/app/stores/subscription.ts b/app/stores/subscription.ts index 8a1060eb..aa242974 100644 --- a/app/stores/subscription.ts +++ b/app/stores/subscription.ts @@ -14,7 +14,7 @@ export const useSubscriptionStore = defineStore('subscription', () => { async function fetchSubscriptions() { try { - const data = await $api('/api/payments/stripe/me') + const data = await useApiStripe().fetchStripeSubscription() customer.value = data.customer currentSubscription.value = data.subscription subscriptions.value = data.subscriptions @@ -24,22 +24,11 @@ export const useSubscriptionStore = defineStore('subscription', () => { } } - async function createSubscriptionCheckoutUrl(customerId: string, priceId: string) { - return $api(`/api/payments/stripe/customers/${customerId}/checkout`, { - method: 'POST', - body: { - priceId, - redirectPath: '/settings/billing-plans', - }, - }) - } - return { customer, subscriptions, isSubscriptionValid, currentSubscription, fetchSubscriptions, - createSubscriptionCheckoutUrl, } }) diff --git a/app/stores/token.ts b/app/stores/token.ts deleted file mode 100644 index 5439d34e..00000000 --- a/app/stores/token.ts +++ /dev/null @@ -1,37 +0,0 @@ -export const useTokenDeviceStore = defineStore('token-device', () => { - const authStore = useAuthStore() - - const tokenDevice = ref(null) - - async function setTokenDevice(token: string) { - try { - tokenDevice.value = token - await $api(`/api/users/${authStore.currentUser?.sub}/devices`, { - method: 'POST', - body: { token }, - }) - } - catch (error) { - console.error(error) - } - } - - async function clearTokenDevice() { - try { - await $api(`/api/users/${authStore.currentUser?.sub}/devices`, { - method: 'DELETE', - body: { token: tokenDevice.value }, - }) - tokenDevice.value = null - } - catch (error) { - console.error(error) - } - } - - return { - tokenDevice, - setTokenDevice, - clearTokenDevice, - } -}) diff --git a/app/utils/api.ts b/app/utils/api.ts index a3f8f6eb..989dfefb 100644 --- a/app/utils/api.ts +++ b/app/utils/api.ts @@ -6,19 +6,15 @@ export const $api = $fetch.create({ retryStatusCodes: [503, 504], // Request interceptor async onRequest({ options }) { - const authStore = useAuthStore() - options.baseURL = String(useRuntimeConfig().public.apiBaseUrl || '/api') options.headers = { ...options.headers, ...useRequestHeaders(['cookie']) /** need this for calls from SSR: https://auth.sidebase.io/guide/authjs/server-side/session-access#session-access-and-route-protection */, - 'Authorization': `Bearer ${authStore.accessToken}`, 'Csrf-Token': useCsrf().csrf, } }, async onResponseError(error) { - const authStore = useAuthStore() const isRequestFromExternalUrl = !String(error.response.url).startsWith(String(useRuntimeConfig().public.appBaseUrl)) switch (error.response?.status) { case 401: @@ -26,7 +22,7 @@ export const $api = $fetch.create({ return try { - await authStore.signOut() + await navigateTo({ path: '/sign-out' }, { external: true }) } catch {} finally { diff --git a/app/utils/i18n.ts b/app/utils/i18n.ts new file mode 100644 index 00000000..fee08676 --- /dev/null +++ b/app/utils/i18n.ts @@ -0,0 +1,24 @@ +/** + * A safe useI18n that have mocked fallbacks in case the app context is not available. + * + * Do not use this widely in the project, only in some plugins, auto-imports that potentially do not + * have the app context like in notifications, $api, etc. + */ +export function useSafeI18n() { + const nuxtApp = useNuxtApp() + + if (nuxtApp.$i18n) { + return nuxtApp.$i18n + } + else { + // mock useI18n + return { + t: (key: string) => key, + locale: 'en', + availableLocales: ['en'], + setLocale: (locale: string) => { + console.warn(`Setting locale to ${locale} is not supported in this mock implementation.`) + }, + } + } +} diff --git a/app/utils/layout.ts b/app/utils/layout.ts index 5697a844..71146b6e 100644 --- a/app/utils/layout.ts +++ b/app/utils/layout.ts @@ -21,11 +21,19 @@ export function createRouteTree(routes: RouteRecordNormalized[] = []): NavItem[] ]) } - if (!route.meta.action || !route.meta.subject || can(route.meta.action, route.meta.subject)) { + const canNavigate = !route.meta.scopes || ( + Array.isArray(route.meta.scopes) + && route.meta.scopes.some((scope: string) => { + const [action, subject] = scope.split(':') as [string, string] + + return can(action, subject) + }) + ) + + if (canNavigate) { tree.push({ ...item, - action: route.meta.action, - subject: route.meta.subject, + scopes: route.meta.scopes, to, }) } diff --git a/docker-compose.service.yml b/docker-compose.service.yml new file mode 100644 index 00000000..16e6c105 --- /dev/null +++ b/docker-compose.service.yml @@ -0,0 +1,98 @@ +version: '3.7' + +services: + postgres: + container_name: nuxt-template-pg + image: postgres:latest + volumes: + - ./docker/postgres:/var/lib/postgresql/data + environment: + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} + POSTGRES_DB: ${POSTGRES_DB:-postgres} + POSTGRES_PORT: ${POSTGRES_PORT:-5432} + ports: + - '${POSTGRES_PORT}:5432' + restart: unless-stopped + + redis: + container_name: nuxt-template-redis + image: redis:latest + volumes: + - ./docker/redis:/data + environment: + REDIS_PASSWORD: ${REDIS_PASSWORD:-redis} + ports: + - '${REDIS_PORT}:6379' + restart: unless-stopped + + mondodb: + container_name: nuxt-template-mongodb + image: mongo:latest + volumes: + - ./docker/mongodb:/data/db + environment: + MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME:-root} + MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD:-password} + ports: + - '27017:27017' + restart: unless-stopped + + mailhog: + container_name: nuxt-template-mailhog + image: mailhog/mailhog:latest + volumes: + - ./docker/mailhog:/data + ports: + - '${SMTP_PORT:-1025}:1025' # SMTP server port + - '${SMTP_WEB_PORT:-8025}:8025' # Web UI port + restart: unless-stopped + + logto: + depends_on: + postgres-logto: + condition: service_healthy + image: svhd/logto:latest + container_name: nuxt-template-logto + entrypoint: ["sh", "-c", "npm run cli db seed -- --swe && npm start"] + volumes: + - ./docker/logto:/opt/logto/packages/core/data + ports: + - '3001:3001' + - '3002:3002' + environment: + - TRUST_PROXY_HEADER=1 + - DB_URL=postgresql://postgres:p0stgr3s@postgres-logto:5432/logto + - ENDPOINT + - ADMIN_ENDPOINT + restart: unless-stopped + + postgres-logto: + image: postgres:latest + container_name: nuxt-template-pg-logto + volumes: + - ./docker/postgres-logto:/var/lib/postgresql/data + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: p0stgr3s + POSTGRES_DB: logto + ports: + - '54333:5432' + healthcheck: + test: ["CMD-SHELL", "pg_isready"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + +volumes: + postgres: + driver: local + pgadmin: + driver: local + redis: + driver: local + mondodb: + driver: local + logto: + driver: local diff --git a/docker-compose.yml b/docker-compose.yml index bf43fbc9..931a555b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,31 +1,12 @@ -services: - postgres: - container_name: nuxt-pg - image: postgres:latest - volumes: - - ./docker/postgres:/var/lib/postgresql/data - environment: - POSTGRES_USER: ${POSTGRES_USER:-postgres} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} - POSTGRES_DB: ${POSTGRES_DB:-postgres} - POSTGRES_HOSTNAME: ${POSTGRES_HOSTNAME:-nuxt-pg} - POSTGRES_PORT: ${POSTGRES_PORT:-5432} - ports: - - "${POSTGRES_PORT}:5432" - restart: unless-stopped - redis: - container_name: nuxt-redis - image: redis:latest - volumes: - - ./docker/redis:/data - environment: - REDIS_PASSWORD: ${REDIS_PASSWORD:-redis} - ports: - - "${REDIS_PORT}:6379" - restart: unless-stopped +version: "3.9" -volumes: - postgres: - driver: local - redis: - driver: local +services: + nuxt-template: + image: "registry.digitalocean.com/thecodeorigin/nuxt-template:latest" + command: > + sh -c "node .output/server/index.mjs" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000"] + interval: 30s + timeout: 10s + retries: 5 diff --git a/e2e/function/dashboard.test.ts b/e2e/function/dashboard.test.ts deleted file mode 100644 index c098d2a7..00000000 --- a/e2e/function/dashboard.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -describe('/dashboard', () => { - it('should display the dashboard', { timeout: 0 }, async () => { - await $page.goto('http://localhost:3000/dashboard') - - const dashboardHTML = await $page.$eval('[data-test="dashboard"]', el => el.innerHTML) - - expect(dashboardHTML).toContain('Welcome to Nuxt Dashboard') - }) -}) diff --git a/e2e/function/list-project-button-navigate.test.ts b/e2e/function/list-project-button-navigate.test.ts deleted file mode 100644 index dd66342a..00000000 --- a/e2e/function/list-project-button-navigate.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import path from 'node:path' -import { fileURLToPath } from 'node:url' -import { kebabCase } from 'lodash-es' - -describe('/projects', () => { - it('should redirect to detail project page', { timeout: 0 }, async () => { - await $page.goto('http://localhost:3000/projects') - - const buttonDetail = await $page.$('[data-test="project-item-button-detail"]:not([disabled])') - if (!buttonDetail) - return - buttonDetail.click() - - await $page.waitForNavigation({ waitUntil: 'networkidle0' }) - - let currentURL = $page.url() - - expect(currentURL).toContain('/projects/') - - await $page.goto('http://localhost:3000/projects') - - await $page.locator('[data-test="button-create-project"]:not([disabled])').click() - - await $page.waitForNavigation({ waitUntil: 'networkidle0' }) - - currentURL = $page.url() - - expect(currentURL).toContain('/projects/create') - }) -}) diff --git a/e2e/function/list-project-search.test.ts b/e2e/function/list-project-search.test.ts deleted file mode 100644 index aa7eed3d..00000000 --- a/e2e/function/list-project-search.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { title } from 'node:process' -import { setTimeout } from 'node:timers/promises' - -describe('/projects', () => { - it('should search the projects match the keyword ', { timeout: 0 }, async () => { - await $page.goto('http://localhost:3000/projects') - - const searchValue = 'test' - - await $page.locator('[data-test="container-input-search-projects"] input[type="text"]').fill(searchValue) - - $page.waitForNetworkIdle({ idleTime: 1000 }) - - const gridProjects = await $page.$('[data-test="grid-list-projects"]') - - if (gridProjects) { - const listTitles = await $page.$$eval('[data-test="project-item-title"]', titles => titles.map(title => title.textContent)) - const listDescriptions = await $page.$$eval('[data-test="project-item-description"]', list => list.map(el => el.textContent)) - - if (!listTitles || listTitles.length === 0) - return - - const check = listTitles.every((title, index) => { - return title?.toLowerCase().includes(searchValue) || listDescriptions[index]?.toLowerCase().includes(searchValue) - }) - - expect(check).toBe(true) - } - - await $page.locator('[data-test="container-select-category-projects"]').click() - - const listCategories = await $page.$$('.v-overlay__content.v-select__content div > .v-list-item') - - expect(listCategories.length).greaterThanOrEqual(1) - - for (const index in listCategories) { - if (index !== '0') { - listCategories[index]?.click() - break - } - } - - $page.waitForNetworkIdle({ idleTime: 1000 }) - - await setTimeout(1000) - - const categoryTitle = await $page.$eval('[data-test="container-select-category-projects"] .v-select__selection-text', el => el.textContent) - - const listTitles = await $page.$$eval('[data-test="project-item-category-title"]', titles => titles.map(title => title.textContent)) - - if (listTitles.length > 0) { - const check = listTitles.every((title) => { - return title === categoryTitle - }) - - expect(check).toBe(true) - } - }) -}) diff --git a/e2e/gui/__screenshots__/shoud-display-the-list-project-page.png b/e2e/gui/__screenshots__/shoud-display-the-list-project-page.png deleted file mode 100644 index d6caf110..00000000 Binary files a/e2e/gui/__screenshots__/shoud-display-the-list-project-page.png and /dev/null differ diff --git a/e2e/gui/__screenshots__/should-display-the-account-setting-page.png b/e2e/gui/__screenshots__/should-display-the-account-setting-page.png deleted file mode 100644 index 921fbdc2..00000000 Binary files a/e2e/gui/__screenshots__/should-display-the-account-setting-page.png and /dev/null differ diff --git a/e2e/gui/__screenshots__/should-display-the-billing-plan-page.png b/e2e/gui/__screenshots__/should-display-the-billing-plan-page.png deleted file mode 100644 index 4436cff9..00000000 Binary files a/e2e/gui/__screenshots__/should-display-the-billing-plan-page.png and /dev/null differ diff --git a/e2e/gui/__screenshots__/should-display-the-create-project-page.png b/e2e/gui/__screenshots__/should-display-the-create-project-page.png deleted file mode 100644 index e7d608a0..00000000 Binary files a/e2e/gui/__screenshots__/should-display-the-create-project-page.png and /dev/null differ diff --git a/e2e/gui/__screenshots__/should-display-the-dashboard.png b/e2e/gui/__screenshots__/should-display-the-dashboard.png deleted file mode 100644 index a910d093..00000000 Binary files a/e2e/gui/__screenshots__/should-display-the-dashboard.png and /dev/null differ diff --git a/e2e/gui/__screenshots__/should-display-the-list-project-page-dark-theme.png b/e2e/gui/__screenshots__/should-display-the-list-project-page-dark-theme.png deleted file mode 100644 index 7eca5402..00000000 Binary files a/e2e/gui/__screenshots__/should-display-the-list-project-page-dark-theme.png and /dev/null differ diff --git a/e2e/gui/__screenshots__/should-display-the-pricing-page.png b/e2e/gui/__screenshots__/should-display-the-pricing-page.png deleted file mode 100644 index 041f1979..00000000 Binary files a/e2e/gui/__screenshots__/should-display-the-pricing-page.png and /dev/null differ diff --git a/e2e/gui/create-project.test.ts b/e2e/gui/create-project.test.ts deleted file mode 100644 index 4a8714b7..00000000 --- a/e2e/gui/create-project.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import path from 'node:path' -import { fileURLToPath } from 'node:url' -import { kebabCase } from 'lodash-es' - -describe('/projects/create', () => { - it('should display the create project page', { timeout: 0 }, async ({ task }) => { - await $page.goto('http://localhost:3000/projects/create') - - // Make sure button reset form project is enable - const resetButtonEnable = await $page.$eval('button[data-test="button-reset-form-create-project"]', button => !button.disabled) - expect(resetButtonEnable).toBe(true) - - const createButtonEnable = await $page.$eval('button[data-test="button-confirm-form-create-project"]', button => !button.disabled) - expect(createButtonEnable).toBe(true) - - const selectFileButtonEnable = await $page.$eval('button[data-test="button-select-project-file"]', button => !button.disabled) - expect(selectFileButtonEnable).toBe(true) - - const inputeSourceProjectURL = await $page.$eval('[data-test="input-project-source-url"] input[type="text"]', input => !input.disabled) - expect(inputeSourceProjectURL).toBe(true) - - const inputProjectTitle = await $page.$eval('[data-test="input-enter-project-title"] input[type="text"]', input => !input.disabled) - expect(inputProjectTitle).toBe(true) - - const textareProjectDescription = await $page.$eval('[data-test="input-enter-project-description"] textarea', textarea => !textarea.disabled) - expect(textareProjectDescription).toBe(true) - - const inputVoiceRecognition = await $page.$eval('[data-test="input-confirm-voice-recognition"] input[type="checkbox"]', input => !input.disabled) - expect(inputVoiceRecognition).toBe(true) - - await $page.locator('[data-test="dropdown-enter-project-category"]').click() - const listOptions = await $page.$('.v-overlay__content.v-select__content') - expect(listOptions).not.toBeNull() - - await $page.locator('[data-test="dropdown-enter-project-origin-language"]').click() - const listOriginLanguages = await $page.$('.v-overlay__content.v-select__content') - expect(listOriginLanguages).not.toBeNull() - - await $page.locator('[data-test="dropdown-enter-project-language"]').click() - const listLanguages = await $page.$('.v-overlay__content.v-select__content') - expect(listLanguages).not.toBeNull() - - await $page.locator('[data-test="dropdown-enter-project-model-AI"]').click() - const listModalAIs = await $page.$('.v-overlay__content.v-select__content') - expect(listModalAIs).not.toBeNull() - - await $page.goto('http://localhost:3000/projects/create') - const [fileChooser] = await Promise.all([ - $page.waitForFileChooser(), - $page.locator('[data-test="button-select-project-file"]').click(), - ]) - await fileChooser.accept(['Random-file-names-for-automation-testing.mp4']) - const buttonRemoveFile = await $page.$eval('button[data-test="button-remove-project-file"]', button => !button.disabled) - expect(buttonRemoveFile).toBe(true) - - await $page.screenshot({ - path: path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '__screenshots__', - `${kebabCase(task.name)}.png`, - ), - }) - }) -}) diff --git a/e2e/gui/dashboard.test.ts b/e2e/gui/dashboard.test.ts deleted file mode 100644 index 1453e562..00000000 --- a/e2e/gui/dashboard.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import path from 'node:path' -import { fileURLToPath } from 'node:url' -import { kebabCase } from 'lodash-es' - -describe('/dashboard', () => { - it('should display the dashboard', { timeout: 0 }, async ({ task }) => { - await $page.goto('http://localhost:3000/dashboard') - - await $page.screenshot({ - path: path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '__screenshots__', - `${kebabCase(task.name)}.png`, - ), - }) - }) -}) diff --git a/e2e/gui/list-project-dark-theme.test.ts b/e2e/gui/list-project-dark-theme.test.ts deleted file mode 100644 index e85ee041..00000000 --- a/e2e/gui/list-project-dark-theme.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import path from 'node:path' -import { fileURLToPath } from 'node:url' -import { setTimeout } from 'node:timers/promises' -import { kebabCase } from 'lodash-es' - -describe('/projects', () => { - it('should display the list project page dark theme', { timeout: 0 }, async ({ task }) => { - await $page.goto('http://localhost:3000/projects') - - await setTimeout(1000) - await $page.locator('[data-test="button-active-popup-theme-switcher"]').click() - - const popup = await $page.waitForSelector('[data-test="popup-theme-switcher"]') - - expect(popup).not.toBeNull() - - await $page.locator('[data-test="button-active-dark-theme"]').click() - - await $page.screenshot({ - path: path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '__screenshots__', - `${kebabCase(task.name)}.png`, - ), - }) - }) -}) diff --git a/e2e/gui/list-project.test.ts b/e2e/gui/list-project.test.ts deleted file mode 100644 index dd5ee940..00000000 --- a/e2e/gui/list-project.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import path from 'node:path' -import { fileURLToPath } from 'node:url' -import { kebabCase } from 'lodash-es' - -describe('/projects', () => { - it('shoud display the list project page', { timeout: 0 }, async ({ task }) => { - await $page.goto('http://localhost:3000/projects') - - const searchInputNotDisabled = await $page.$eval('[data-test="container-input-search-projects"] input[type="text"]', el => !el.disabled) - expect(searchInputNotDisabled).toBe(true) - - await $page.locator('[data-test="container-select-category-projects"]').click() - const listCategories = await $page.$$('.v-overlay__content.v-select__content div > .v-list-item') - expect(listCategories.length).greaterThanOrEqual(1) - - const buttonCreateProject = await $page.$('[data-test="button-create-project"]:not([disabled])') - expect(buttonCreateProject).not.toBeNull() - - const gridProjects = await $page.$('[data-test="grid-list-projects"]') - if (gridProjects) { - await $page.locator('[data-test="grid-list-projects"] div:first-child input[type="checkbox"]').click() - const deleteButton = await $page.$('[data-test="button-delete-project"]') - expect(deleteButton).not.toBeNull() - - const buttonDetail = await $page.$('[data-test="project-item-button-detail"]') - expect(buttonDetail).not.toBeNull() - - const paginationContainer = await $page.$('[data-test="v-pagination-root"]') - expect(paginationContainer).not.toBeNull() - } - - await $page.screenshot({ - path: path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '__screenshots__', - `${kebabCase(task.name)}.png`, - ), - }) - }) -}) diff --git a/e2e/gui/setting-account.test.ts b/e2e/gui/setting-account.test.ts deleted file mode 100644 index 08d6156e..00000000 --- a/e2e/gui/setting-account.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import path from 'node:path' -import { fileURLToPath } from 'node:url' -import { kebabCase } from 'lodash-es' - -describe('/settings/account', () => { - it('should display the account setting page', { timeout: 0 }, async ({ task }) => { - await $page.goto('http://localhost:3000/settings/account') - - await $page.waitForSelector('[data-test="account-form"]') - - const uploadButton = await $page.$('[data-test="upload-photo-button"]') - expect(uploadButton).not.toBeNull() - const isDisabled = await uploadButton!.evaluate((button: any) => button.disabled) - expect(isDisabled).toBe(false) - - const resetButton = await $page.$('[data-test="reset-photo-button"]') - expect(resetButton).not.toBeNull() - const isResetDisabled = await resetButton?.evaluate((button: any) => button.disabled) - expect(isResetDisabled).toBe(false) - - const fullnameInputWrapper = await $page.$('[data-test="full-name-input"]') - expect(fullnameInputWrapper).not.toBeNull() - const fullnameInput = await fullnameInputWrapper?.$('input') - expect(fullnameInput).not.toBeNull() - const fullnameInputValue = await fullnameInput!.evaluate((input: any) => input.disabled) - expect(fullnameInputValue).toBe(false) - - const emailInputWrapper = await $page.$('[data-test="email-input"]') - expect(emailInputWrapper).not.toBeNull() - const emailInput = await emailInputWrapper?.$('input') - expect(emailInput).not.toBeNull() - const isEmailDisabled = await emailInput!.evaluate((input: any) => input.disabled) - expect(isEmailDisabled).toBe(true) - - const organizationInputWrapper = await $page.$('[data-test="organization-input"]') - expect(organizationInputWrapper).not.toBeNull() - const organizationInput = await organizationInputWrapper?.$('input') - expect(organizationInput).not.toBeNull() - const isOrganizationDisabled = await organizationInput!.evaluate((input: any) => input.disabled) - expect(isOrganizationDisabled).toBe(false) - - const phoneInputWrapper = await $page.$('[data-test="phone-input"]') - expect(phoneInputWrapper).not.toBeNull() - const phoneInput = await phoneInputWrapper?.$('input') - expect(phoneInput).not.toBeNull() - const isPhoneDisabled = await phoneInput!.evaluate((input: any) => input.disabled) - expect(isPhoneDisabled).toBe(false) - - const addressInputWrapper = await $page.$('[data-test="address-input"]') - expect(addressInputWrapper).not.toBeNull() - const addressInput = await addressInputWrapper?.$('input') - expect(addressInput).not.toBeNull() - const isAddressDisabled = await addressInput!.evaluate((input: any) => input.disabled) - expect(isAddressDisabled).toBe(false) - - const zipCodeInputWrapper = await $page.$('[data-test="zip-code-input"]') - expect(zipCodeInputWrapper).not.toBeNull() - const zipCodeInput = await zipCodeInputWrapper?.$('input') - expect(zipCodeInput).not.toBeNull() - const isZipCodeDisabled = await zipCodeInput!.evaluate((input: any) => input.disabled) - expect(isZipCodeDisabled).toBe(false) - - const saveButton = await $page.$('[data-test="save-button"]') - expect(saveButton).not.toBeNull() - const isSaveDisabled = await saveButton?.evaluate((button: any) => button.disabled) - expect(isSaveDisabled).toBe(false) - - const resetButton2 = await $page.$('[data-test="reset-button"]') - expect(resetButton2).not.toBeNull() - const isResetDisabled2 = await resetButton2?.evaluate((button: any) => button.disabled) - expect(isResetDisabled2).toBe(false) - - await $page.screenshot({ - path: path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '__screenshots__', - `${kebabCase(task.name)}.png`, - ), - }) - }) -}) diff --git a/e2e/gui/setting-billingplans.test.ts b/e2e/gui/setting-billingplans.test.ts deleted file mode 100644 index c33e6b72..00000000 --- a/e2e/gui/setting-billingplans.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import path from 'node:path' -import { fileURLToPath } from 'node:url' -import { kebabCase } from 'lodash-es' - -describe('/settings/billing-plans', () => { - it('should display the billing plan page', { timeout: 0 }, async ({ task }) => { - await $page.goto('http://localhost:3000/settings/billing-plans') - - await $page.waitForSelector('[data-test="current-plan-component"]') - - const title = await $page.$('[data-test="plan-name"]') - expect(title).not.toBeNull() - - const expiredDate = await $page.$('[data-test="plan-expired-date"]') - expect(expiredDate).not.toBeNull() - - const planPrice = await $page.$('[data-test="plan-price"]') - expect(planPrice).not.toBeNull() - - await $page.screenshot({ - path: path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '__screenshots__', - `${kebabCase(task.name)}.png`, - ), - }) - }) -}) diff --git a/e2e/gui/setting-pricing.test.ts b/e2e/gui/setting-pricing.test.ts deleted file mode 100644 index 3040e592..00000000 --- a/e2e/gui/setting-pricing.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import path from 'node:path' -import { fileURLToPath } from 'node:url' -import { kebabCase } from 'lodash-es' - -describe('/settings/pricing', () => { - it('should display the pricing page', { timeout: 0 }, async ({ task }) => { - await $page.goto('http://localhost:3000/settings/pricing') - - await $page.waitForSelector('[data-test="pricing-list"]') - - const price = await $page.$('[data-test="pricing-price"]') - expect(price).not.toBeNull() - - // Check if the current plan button is disabled - const currentPlanButton = await $page.$('[data-test="current-plan-button"]') - expect(currentPlanButton).not.toBeNull() - const isDisabled = await currentPlanButton!.evaluate((button: any) => button.disabled) - expect(isDisabled).toBe(true) - - // Check if the upgrade plan button is enabled - const upgradePlanButton = await $page.$('[data-test="upgrade-plan-button"]') - expect(upgradePlanButton).not.toBeNull() - const isUpgradeEnabled = await upgradePlanButton!.evaluate((button: any) => button.disabled) - expect(isUpgradeEnabled).toBe(false) - - await $page.screenshot({ - path: path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '__screenshots__', - `${kebabCase(task.name)}.png`, - ), - }) - }) -}) diff --git a/e2e/setup/1.init.ts b/e2e/setup/1.init.ts deleted file mode 100644 index 68e4d30f..00000000 --- a/e2e/setup/1.init.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { CookieParam } from 'puppeteer' -import puppeteer from 'puppeteer' -import cookies from './cookies.json' - -function createCookie(name: string, value: string): CookieParam { - return { - domain: 'localhost', - url: 'http://localhost:3000', - name, - value, - } -} - -beforeAll(async () => { - const browser = await puppeteer.launch({ slowMo: 50, args: ['--disable-notifications'] }) - const page = await browser.newPage() - - await page.setViewport({ width: 1920, height: 1080 }) - - await page.setCookie(...cookies.map(({ name, value }) => createCookie(name, value))) - - globalThis.$page = page - - return async () => { - await browser.close() - } -}) diff --git a/e2e/setup/cookies.json b/e2e/setup/cookies.json deleted file mode 100644 index f1896d43..00000000 --- a/e2e/setup/cookies.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "name": "nuxt-csrf-token", - "value": "c78f852528dea97ae1b5ae73c91acddbbbaa7a5f483add8c58973cc238fcd008%7Ccb7f002cca456aa275f73f5082d7d86b8b5ecfd071a965640c95159293386193" - }, - { - "name": "nuxt-callback-url", - "value": "http://localhost:3000/dashboard" - }, - { - "name": "nuxt-session-token", - "value": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..xX1f-QdMLdEjHLMF.51UJaBnioxjEYgr5bzjGnRam3VbQzJxMyI5zXNxGkt-7R6XatLJZDMLtOMA6KM5dKNwUM56WFJvtmia-SNDys-OxujyLamVetpFswHqoqYDa41QOI3RhSasTKuuUpDuXIncddFvde5dp2njUihHZBpFimfuTIqE5s-6-cRheFK5O9cafKJGU_rr7nNzbUp1Omio50hLNaswUiGOdcy1_eZfy_wAaV3aivjFuFvX_omuqmnVGfi3SqkJtTLI2PGay99pgYEGmhnmbEgaHnT60n3gNaDJZW_FcKWDw_qpbzThVk6Niv4Wt_RwOjNg85JSlY3Y8wG5wDNQXjpJ-o6qfMpEz2aZF1ij8alTYUwQM6y6wGUpK4cYhSA8m--9tHTT3pPx-BakkVQub.T3X0pRe9aKe-yKgpN6fBCA" - } -] diff --git a/env.d.ts b/env.d.ts index 9f3ca283..cb757cc0 100644 --- a/env.d.ts +++ b/env.d.ts @@ -1,6 +1,5 @@ import type { RouteLocationRaw } from 'vue-router' import type { Arrayable } from '@vueuse/core' -import type { Page } from 'puppeteer' import type { NavGroupType, NavItem } from '@base/@layouts/types' import type { z } from 'zod' import type { HookResult } from '@nuxt/schema' @@ -19,11 +18,6 @@ declare module 'vue-router' { } } -declare global { - // eslint-disable-next-line vars-on-top - var $page: Page -} - declare module '#app' { interface RuntimeNuxtHooks { 'session:refresh': () => HookResult diff --git a/modules/sentry/runtime/plugin.ts b/modules/sentry/runtime/plugin.ts index 08dec383..12d05f65 100644 --- a/modules/sentry/runtime/plugin.ts +++ b/modules/sentry/runtime/plugin.ts @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser' export default defineNuxtPlugin({ name: 'sentry', parallel: true, - dependsOn: ['auth'], setup(nuxtApp) { const config = useRuntimeConfig() diff --git a/nuxt.config.ts b/nuxt.config.ts index 317bffad..c680a348 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -98,10 +98,6 @@ export default defineNuxtConfig({ authorization: Boolean(process.env.FEATURE_AUTHORIZATION), }, - hotjar: { - projectId: process.env.HOTJAR_ID, - }, - theme: { appLogo: process.env.NUXT_PUBLIC_APP_LOGO || '/images/logo.svg', appName: process.env.NUXT_PUBLIC_APP_NAME || 'nuxt-template', @@ -154,6 +150,7 @@ export default defineNuxtConfig({ dirs: [ fileURLToPath(new URL('./app/@core/utils', import.meta.url)), fileURLToPath(new URL('./app/@core/composable', import.meta.url)), + fileURLToPath(new URL('./app/api', import.meta.url)), ], }, @@ -219,13 +216,23 @@ export default defineNuxtConfig({ replace: { 'import-in-the-middle': fileURLToPath(new URL('./node_modules/import-in-the-middle', import.meta.url)), }, + + imports: { + dirs: [ + fileURLToPath(new URL('./server/composables', import.meta.url)), + ], + }, }, routeRules: { - '/': { prerender: true }, - '/api/payments/**/callback': { csurf: false }, - '/api/payments/**/webhook': { csurf: false }, - '/api/payments/**/IPN': { csurf: false }, + '/auth/**': { swr: false, prerender: false }, + '/callback': { cache: false, prerender: false, security: { enabled: false } }, + '/api/logto/webhook': { csurf: false }, + '/api/payments/sepay/webhook': { csurf: false }, + '/api/payments/payos/webhook': { csurf: false }, + '/api/payments/vnpay/callback': { csurf: false }, + '/api/payments/vnpay/IPN': { csurf: false }, + '/_nitro/tasks/**': { csurf: false }, }, pinia: { @@ -284,7 +291,6 @@ export default defineNuxtConfig({ 'nuxt-security', 'nuxt-vuefire', 'nuxt-gtag', - 'nuxt-module-hotjar', 'nuxt-nodemailer', ], @@ -310,12 +316,6 @@ export default defineNuxtConfig({ ], }, - hotjar: { - hotjarId: process.env.HOTJAR_ID, - scriptVersion: 6, - debug: process.env.NODE_ENV === 'development', - }, - nodemailer: process.env.NODE_ENV === 'development' ? { from: process.env.SMTP_FROM, @@ -349,16 +349,28 @@ export default defineNuxtConfig({ }, }, hidePoweredBy: true, - rateLimiter: { - driver: { - name: 'redis', - options: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - password: process.env.REDIS_PASSWORD, - }, - }, - }, + rateLimiter: process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN + ? { + driver: { + name: 'upstash', + options: { + url: process.env.UPSTASH_REDIS_REST_URL, + token: process.env.UPSTASH_REDIS_REST_TOKEN, + }, + }, + } + : process.env.REDIS_HOST && process.env.REDIS_PASSWORD + ? { + driver: { + name: 'redis', + options: { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + password: process.env.REDIS_PASSWORD, + }, + }, + } + : false, }, compatibilityDate: '2024-07-12', diff --git a/package.json b/package.json index 416c2b6a..b30653cf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@thecodeorigin/nuxt", "type": "module", - "version": "1.11.4", + "version": "1.12.0-rc.9", "publishConfig": { "registry": "https://registry.npmjs.org", "access": "public" @@ -43,10 +43,10 @@ "db:preview": "drizzle-kit studio --port 3001", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", - "db:seed": "jiti ./server/db/seeds/index.seed.ts", - "docker:clear": "rimraf ./docker/postgres ./docker/redis", - "docker:start": "docker compose up -d", - "docker:stop": "docker-compose down", + "db:seed": "jiti ./server/db/seeds/index.ts", + "docker:clear": "rimraf ./docker", + "docker:start": "docker compose -f docker-compose.service.yml up -d --force-recreate", + "docker:stop": "docker-compose -f docker-compose.service.yml down", "docker:reset": "pnpm docker:stop && pnpm docker:clear && pnpm docker:start --force-recreate", "gettext:extract": "vue-gettext-extract", "gettext:compile": "vue-gettext-compile" @@ -84,8 +84,6 @@ "@pinia/nuxt": "^0.5.1", "@sentry/nuxt": "^8.36.0", "@sindresorhus/is": "6.3.0", - "@stylistic/stylelint-config": "1.0.1", - "@stylistic/stylelint-plugin": "2.1.1", "@thecodeorigin/eslint-config": "workspace:*", "@tiptap/extension-character-count": "2.3.0", "@tiptap/extension-highlight": "2.3.0", @@ -109,6 +107,7 @@ "@types/webfontloader": "1.6.38", "@typescript-eslint/eslint-plugin": "7.7.1", "@typescript-eslint/parser": "7.7.1", + "@upstash/redis": "^1.35.0", "@videojs-player/vue": "1.0.0", "@vueuse/core": "10.9.0", "@vueuse/math": "10.9.0", @@ -120,21 +119,23 @@ "date-fns": "^4.1.0", "docx": "^8.5.0", "dotenv": "^16.4.5", - "drizzle-kit": "^0.23.0", - "drizzle-orm": "^0.32.0", - "drizzle-zod": "^0.5.1", + "drizzle-kit": "^0.30.5", + "drizzle-orm": "^0.40.0", + "drizzle-zod": "^0.7.0", "eslint": "8.57.0", "execa": "^9.3.0", - "firebase": "^10.12.4", - "firebase-admin": "^12.2.0", - "firebase-functions": "^5.0.1", + "firebase": "^11.4.0", + "firebase-admin": "^13.2.0", "h3": "^1.13.0", + "ioredis": "^5.6.1", + "jiti": "^2.4.2", "lodash-es": "^4.17.21", "mapbox-gl": "3.2.0", - "nodemailer": "^6.9.15", + "mongodb": "^6.14.2", + "nanoid": "^5.1.5", + "nodemailer": "^6.10.0", "nuxt": "^3.13.2", "nuxt-gtag": "^3.0.2", - "nuxt-module-hotjar": "^1.3.2", "nuxt-nodemailer": "^1.1.2", "nuxt-security": "^2.1.4", "nuxt-vuefire": "^1.0.3", @@ -146,7 +147,6 @@ "postcss-scss": "4.0.9", "postgres": "^3.4.3", "prismjs": "1.29.0", - "puppeteer": "^23.4.1", "rimraf": "^6.0.1", "roboto-fontface": "0.10.0", "sass": "1.75.0", @@ -164,6 +164,7 @@ "ufo": "1.5.3", "unbuild": "^2.0.0", "unplugin-vue-define-options": "1.4.3", + "unstorage": "^1.16.0", "video.js": "8.6.0", "vite": "5.2.10", "vite-plugin-vuetify": "2.0.3", @@ -182,6 +183,8 @@ "vuefire": "^3.1.24", "vuetify": "3.5.15", "vuetify-nuxt-module": "^0.18.3", + "winston": "^3.17.0", + "winston-daily-rotate-file": "^5.0.0", "zod": "^3.23.8" }, "resolutions": { diff --git a/packages/config/eslint/index.mjs b/packages/config/eslint/index.mjs index 90e3fe83..ff4ee84a 100644 --- a/packages/config/eslint/index.mjs +++ b/packages/config/eslint/index.mjs @@ -12,10 +12,6 @@ const defaultOptions = { }, vue: true, jsonc: true, - stylistic: { - indent: 2, - quotes: 'single', - }, rules: { 'no-console': 'off', 'no-debugger': 'warn', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d61c52b..d81cf3f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,7 +86,7 @@ importers: version: 0.5.7(eslint@8.57.0)(magicast@0.3.5)(rollup@3.29.5)(typescript@5.4.5)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(webpack-sources@3.2.3) '@nuxt/fonts': specifier: ^0.9.2 - version: 0.9.2(encoding@0.1.13)(ioredis@5.4.1)(magicast@0.3.5)(rollup@3.29.5)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(webpack-sources@3.2.3) + version: 0.9.2(@upstash/redis@1.35.0)(db0@0.1.4(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4)))(encoding@0.1.13)(ioredis@5.6.1)(magicast@0.3.5)(rollup@3.29.5)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(webpack-sources@3.2.3) '@nuxt/kit': specifier: ^3.13.2 version: 3.13.2(magicast@0.3.5)(rollup@3.29.5)(webpack-sources@3.2.3) @@ -107,16 +107,10 @@ importers: version: 0.5.1(magicast@0.3.5)(rollup@3.29.5)(typescript@5.4.5)(vue@3.5.8(typescript@5.4.5))(webpack-sources@3.2.3) '@sentry/nuxt': specifier: ^8.36.0 - version: 8.37.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.27.0)(encoding@0.1.13)(magicast@0.3.5)(nuxt@3.13.2(@parcel/watcher@2.4.1)(@types/node@20.12.7)(drizzle-orm@0.32.2(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(pg@8.12.0)(postgres@3.4.4)(react@18.2.0))(encoding@0.1.13)(eslint@8.57.0)(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.3)(rollup@3.29.5)(sass@1.75.0)(stylelint@16.2.1(typescript@5.4.5))(terser@5.30.4)(typescript@5.4.5)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(webpack-sources@3.2.3))(pinia@2.1.7(typescript@5.4.5)(vue@3.5.8(typescript@5.4.5)))(rollup@3.29.5)(vue@3.5.8(typescript@5.4.5))(webpack-sources@3.2.3) + version: 8.37.0(bd6072b168615c8ea944b69150988573) '@sindresorhus/is': specifier: 6.3.0 version: 6.3.0 - '@stylistic/stylelint-config': - specifier: 1.0.1 - version: 1.0.1(stylelint@16.2.1(typescript@5.4.5)) - '@stylistic/stylelint-plugin': - specifier: 2.1.1 - version: 2.1.1(stylelint@16.2.1(typescript@5.4.5)) '@thecodeorigin/eslint-config': specifier: workspace:* version: link:packages/config/eslint @@ -186,6 +180,9 @@ importers: '@typescript-eslint/parser': specifier: 7.7.1 version: 7.7.1(eslint@8.57.0)(typescript@5.4.5) + '@upstash/redis': + specifier: ^1.35.0 + version: 1.35.0 '@videojs-player/vue': specifier: 1.0.0 version: 1.0.0(@types/video.js@7.3.58)(video.js@8.6.0)(vue@3.5.8(typescript@5.4.5)) @@ -197,7 +194,7 @@ importers: version: 10.9.0(vue@3.5.8(typescript@5.4.5)) '@vueuse/nuxt': specifier: ^10.11.0 - version: 10.11.0(magicast@0.3.5)(nuxt@3.13.2(@parcel/watcher@2.4.1)(@types/node@20.12.7)(drizzle-orm@0.32.2(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(pg@8.12.0)(postgres@3.4.4)(react@18.2.0))(encoding@0.1.13)(eslint@8.57.0)(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.3)(rollup@3.29.5)(sass@1.75.0)(stylelint@16.2.1(typescript@5.4.5))(terser@5.30.4)(typescript@5.4.5)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(webpack-sources@3.2.3))(rollup@3.29.5)(vue@3.5.8(typescript@5.4.5))(webpack-sources@3.2.3) + version: 10.11.0(magicast@0.3.5)(nuxt@3.13.2(@parcel/watcher@2.4.1)(@types/node@20.12.7)(@upstash/redis@1.35.0)(db0@0.1.4(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4)))(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4))(encoding@0.1.13)(eslint@8.57.0)(ioredis@5.6.1)(magicast@0.3.5)(optionator@0.9.3)(rollup@3.29.5)(sass@1.75.0)(stylelint@16.2.1(typescript@5.4.5))(terser@5.30.4)(typescript@5.4.5)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(webpack-sources@3.2.3))(rollup@3.29.5)(vue@3.5.8(typescript@5.4.5))(webpack-sources@3.2.3) apexcharts: specifier: 3.49.0 version: 3.49.0 @@ -220,59 +217,65 @@ importers: specifier: ^16.4.5 version: 16.4.5 drizzle-kit: - specifier: ^0.23.0 - version: 0.23.2 + specifier: ^0.30.5 + version: 0.30.6 drizzle-orm: - specifier: ^0.32.0 - version: 0.32.2(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(pg@8.12.0)(postgres@3.4.4)(react@18.2.0) + specifier: ^0.40.0 + version: 0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4) drizzle-zod: - specifier: ^0.5.1 - version: 0.5.1(drizzle-orm@0.32.2(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(pg@8.12.0)(postgres@3.4.4)(react@18.2.0))(zod@3.23.8) + specifier: ^0.7.0 + version: 0.7.1(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4))(zod@3.23.8) eslint: specifier: 8.57.0 version: 8.57.0 execa: specifier: ^9.3.0 - version: 9.3.0 + version: 9.6.0 firebase: - specifier: ^10.12.4 - version: 10.12.4 + specifier: ^11.4.0 + version: 11.9.0 firebase-admin: - specifier: ^12.2.0 - version: 12.2.0(encoding@0.1.13) - firebase-functions: - specifier: ^5.0.1 - version: 5.0.1(firebase-admin@12.2.0(encoding@0.1.13)) + specifier: ^13.2.0 + version: 13.4.0(encoding@0.1.13) h3: specifier: ^1.13.0 version: 1.13.0 + ioredis: + specifier: ^5.6.1 + version: 5.6.1 + jiti: + specifier: ^2.4.2 + version: 2.4.2 lodash-es: specifier: ^4.17.21 version: 4.17.21 mapbox-gl: specifier: 3.2.0 version: 3.2.0 + mongodb: + specifier: ^6.14.2 + version: 6.17.0 + nanoid: + specifier: ^5.1.5 + version: 5.1.5 nodemailer: - specifier: ^6.9.15 - version: 6.9.15 + specifier: ^6.10.0 + version: 6.10.1 nuxt: specifier: ^3.13.2 - version: 3.13.2(@parcel/watcher@2.4.1)(@types/node@20.12.7)(drizzle-orm@0.32.2(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(pg@8.12.0)(postgres@3.4.4)(react@18.2.0))(encoding@0.1.13)(eslint@8.57.0)(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.3)(rollup@3.29.5)(sass@1.75.0)(stylelint@16.2.1(typescript@5.4.5))(terser@5.30.4)(typescript@5.4.5)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(webpack-sources@3.2.3) + version: 3.13.2(@parcel/watcher@2.4.1)(@types/node@20.12.7)(@upstash/redis@1.35.0)(db0@0.1.4(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4)))(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4))(encoding@0.1.13)(eslint@8.57.0)(ioredis@5.6.1)(magicast@0.3.5)(optionator@0.9.3)(rollup@3.29.5)(sass@1.75.0)(stylelint@16.2.1(typescript@5.4.5))(terser@5.30.4)(typescript@5.4.5)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(webpack-sources@3.2.3) nuxt-gtag: specifier: ^3.0.2 version: 3.0.2(magicast@0.3.5)(rollup@3.29.5)(webpack-sources@3.2.3) - nuxt-module-hotjar: - specifier: ^1.3.2 - version: 1.3.2(magicast@0.3.5)(rollup@3.29.5)(webpack-sources@3.2.3) nuxt-nodemailer: specifier: ^1.1.2 - version: 1.1.2(magicast@0.3.5)(nodemailer@6.9.15)(rollup@3.29.5)(webpack-sources@3.2.3) + version: 1.1.2(magicast@0.3.5)(nodemailer@6.10.1)(rollup@3.29.5)(webpack-sources@3.2.3) nuxt-security: specifier: ^2.1.4 version: 2.1.4(magicast@0.3.5)(rollup@3.29.5)(webpack-sources@3.2.3) nuxt-vuefire: specifier: ^1.0.3 - version: 1.0.3(@firebase/app-types@0.9.2)(firebase-admin@12.2.0(encoding@0.1.13))(firebase-functions@5.0.1(firebase-admin@12.2.0(encoding@0.1.13)))(firebase@10.12.4)(magicast@0.3.5)(rollup@3.29.5)(vuefire@3.1.24(consola@3.4.0)(firebase@10.12.4)(vue@3.5.8(typescript@5.4.5)))(webpack-sources@3.2.3) + version: 1.0.3(@firebase/app-types@0.9.2)(firebase-admin@13.4.0(encoding@0.1.13))(firebase-functions@5.0.1(firebase-admin@13.4.0(encoding@0.1.13)))(firebase@11.9.0)(magicast@0.3.5)(rollup@3.29.5)(vuefire@3.1.24(consola@3.4.0)(firebase@11.9.0)(vue@3.5.8(typescript@5.4.5)))(webpack-sources@3.2.3) ofetch: specifier: 1.3.4 version: 1.3.4 @@ -297,9 +300,6 @@ importers: prismjs: specifier: 1.29.0 version: 1.29.0 - puppeteer: - specifier: ^23.4.1 - version: 23.4.1(typescript@5.4.5) rimraf: specifier: ^6.0.1 version: 6.0.1 @@ -351,6 +351,9 @@ importers: unplugin-vue-define-options: specifier: 1.4.3 version: 1.4.3(rollup@3.29.5)(vue@3.5.8(typescript@5.4.5))(webpack-sources@3.2.3) + unstorage: + specifier: ^1.16.0 + version: 1.16.0(@upstash/redis@1.35.0)(db0@0.1.4(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4)))(ioredis@5.6.1) video.js: specifier: 8.6.0 version: 8.6.0 @@ -398,13 +401,19 @@ importers: version: 2.0.0(vue@3.5.8(typescript@5.4.5)) vuefire: specifier: ^3.1.24 - version: 3.1.24(consola@3.4.0)(firebase@10.12.4)(vue@3.5.8(typescript@5.4.5)) + version: 3.1.24(consola@3.4.0)(firebase@11.9.0)(vue@3.5.8(typescript@5.4.5)) vuetify: specifier: 3.5.15 version: 3.5.15(typescript@5.4.5)(vite-plugin-vuetify@2.0.3)(vue-i18n@9.13.1(vue@3.5.8(typescript@5.4.5)))(vue@3.5.8(typescript@5.4.5)) vuetify-nuxt-module: specifier: ^0.18.3 version: 0.18.3(magicast@0.3.5)(rollup@3.29.5)(typescript@5.4.5)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(vue@3.5.8(typescript@5.4.5))(webpack-sources@3.2.3) + winston: + specifier: ^3.17.0 + version: 3.17.0 + winston-daily-rotate-file: + specifier: ^5.0.0 + version: 5.0.0(winston@3.17.0) zod: specifier: ^3.23.8 version: 3.23.8 @@ -413,7 +422,7 @@ importers: dependencies: '@antfu/eslint-config': specifier: ^2.22.0 - version: 2.22.0(@vue/compiler-sfc@3.5.8)(eslint@8.57.0)(typescript@5.4.5)(vitest@2.1.1(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4)) + version: 2.22.0(@vue/compiler-sfc@3.5.8)(eslint@8.57.0)(typescript@5.4.5)(vitest@2.1.1(@types/node@22.15.30)(sass@1.75.0)(terser@5.30.4)) '@antfu/eslint-config-vue': specifier: 0.43.1 version: 0.43.1(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) @@ -430,17 +439,20 @@ packages: '@antfu/eslint-config-basic@0.43.1': resolution: {integrity: sha512-SW6hmGmqI985fsCJ+oivo4MbiMmRMgCJ0Ne8j/hwCB6O6Mc0m5bDqYeKn5HqFhvZhG84GEg5jPDKNiHrBYnQjw==} + deprecated: Deprecated, please migrate to @antfu/eslint-config with the flat config peerDependencies: eslint: '>=7.4.0' '@antfu/eslint-config-ts@0.43.1': resolution: {integrity: sha512-s3zItBSopYbM/3eii/JKas1PmWR+wCPRNS89qUi4zxPvpuIgN5mahkBvbsCiWacrNFtLxe1zGgo5qijBhVfuvA==} + deprecated: Deprecated, please migrate to @antfu/eslint-config with the flat config peerDependencies: eslint: '>=7.4.0' typescript: '>=3.9' '@antfu/eslint-config-vue@0.43.1': resolution: {integrity: sha512-HxOfe8Vl+DPrzssbs5LHRDCnBtCy1LSA1DIeV71IC+iTpzoASFahSsVX5qckYu1InFgUm93XOhHCWm34LzPsvg==} + deprecated: Deprecated, please migrate to @antfu/eslint-config with the flat config peerDependencies: eslint: '>=7.4.0' @@ -947,6 +959,10 @@ packages: resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==} engines: {node: '>=16.13'} + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + '@csstools/css-parser-algorithms@2.6.1': resolution: {integrity: sha512-ubEkAaTfVZa+WwGhs5jbo5Xfqpeaybr/RvWzvFxRs4jfq16wH8l8Ty/QEEpINxll4xhuGfdMbipRyz5QZh9+FA==} engines: {node: ^14 || ^16 || >=18} @@ -970,8 +986,11 @@ packages: peerDependencies: postcss-selector-parser: ^6.0.13 - '@drizzle-team/brocli@0.8.2': - resolution: {integrity: sha512-zTrFENsqGvOkBOuHDC1pXCkDXNd2UhP4lI3gYGhQ1R1SPeAAfqzPsV1dcpMy4uNU6kB5VpU5NGhvwxVNETR02A==} + '@dabh/diagnostics@2.0.3': + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} '@es-joy/jsdoccomment@0.41.0': resolution: {integrity: sha512-aKUhyn1QI5Ksbqcr3fFJj16p99QdjUxXAEuFst1Z47DRyoiMwivIH9MV/ARcJOCXVjPfjITciej8ZD2O/6qUmw==} @@ -1879,60 +1898,79 @@ packages: resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} - '@firebase/analytics-compat@0.2.12': - resolution: {integrity: sha512-rXWnOAdEHbvBPLNjFLu3U0yDZVIAi+C0DL+RkUEOirfSqAeQaKzBCATeBw6+K7FVpEnknhm4tZrvVUVtJjShMw==} + '@fastify/busboy@3.1.1': + resolution: {integrity: sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==} + + '@firebase/ai@1.4.0': + resolution: {integrity: sha512-wvF33gtU6TXb6Co8TEC1pcl4dnVstYmRE/vs9XjUGE7he7Sgf5TqSu+EoXk/fuzhw5tKr1LC5eG9KdYFM+eosw==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + '@firebase/app-types': 0.x + + '@firebase/analytics-compat@0.2.22': + resolution: {integrity: sha512-VogWHgwkdYhjWKh8O1XU04uPrRaiDihkWvE/EMMmtWtaUtVALnpLnUurc3QtSKdPnvTz5uaIGKlW84DGtSPFbw==} peerDependencies: '@firebase/app-compat': 0.x - '@firebase/analytics-types@0.8.2': - resolution: {integrity: sha512-EnzNNLh+9/sJsimsA/FGqzakmrAUKLeJvjRHlg8df1f97NLUlFidk9600y0ZgWOp3CAxn6Hjtk+08tixlUOWyw==} + '@firebase/analytics-types@0.8.3': + resolution: {integrity: sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==} - '@firebase/analytics@0.10.6': - resolution: {integrity: sha512-sB59EwcAvLt0fINGfMWmcRKcdUiYhE4AJNdDXSCSDo4D/ZXFRmb6qwX9YesKHXFB59XTLT03mAjqQcDrdym9qA==} + '@firebase/analytics@0.10.16': + resolution: {integrity: sha512-cMtp19He7Fd6uaj/nDEul+8JwvJsN8aRSJyuA1QN3QrKvfDDp+efjVurJO61sJpkVftw9O9nNMdhFbRcTmTfRQ==} peerDependencies: '@firebase/app': 0.x - '@firebase/app-check-compat@0.3.13': - resolution: {integrity: sha512-1sbS5Apq7dLys1KYdNQsmZLFIjJoFP9Mv4bzIcdXuTkWQjr3X2qAvwiTslC6prVAUMiTV0eM9eicdQIXVsiSRw==} + '@firebase/app-check-compat@0.3.25': + resolution: {integrity: sha512-3zrsPZWAKfV7DVC20T2dgfjzjtQnSJS65OfMOiddMUtJL1S5i0nAZKsdX0bOEvvrd0SBIL8jYnfpfDeQRnhV3w==} + engines: {node: '>=18.0.0'} peerDependencies: '@firebase/app-compat': 0.x - '@firebase/app-check-interop-types@0.3.2': - resolution: {integrity: sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ==} + '@firebase/app-check-interop-types@0.3.3': + resolution: {integrity: sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==} - '@firebase/app-check-types@0.5.2': - resolution: {integrity: sha512-FSOEzTzL5bLUbD2co3Zut46iyPWML6xc4x+78TeaXMSuJap5QObfb+rVvZJtla3asN4RwU7elaQaduP+HFizDA==} + '@firebase/app-check-types@0.5.3': + resolution: {integrity: sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==} - '@firebase/app-check@0.8.6': - resolution: {integrity: sha512-uSzl0/SDw54hwuORWHDtldb9kK/QEVZOcoPn2mlIjMrJOLDug/6kcqnIN3IHzwmPyf23Epg0AGBktvG2FugW4w==} + '@firebase/app-check@0.10.0': + resolution: {integrity: sha512-AZlRlVWKcu8BH4Yf8B5EI8sOi2UNGTS8oMuthV45tbt6OVUTSQwFPIEboZzhNJNKY+fPsg7hH8vixUWFZ3lrhw==} + engines: {node: '>=18.0.0'} peerDependencies: '@firebase/app': 0.x - '@firebase/app-compat@0.2.37': - resolution: {integrity: sha512-yiQLYT9LYQHuJGu/msuBLFtdWWTJ3Pz04E9gSeWykSB+8s0XXJJqfqQlghH7CcQ3KnJZR+Wuc3zSMcY3a+dn6Q==} + '@firebase/app-compat@0.4.1': + resolution: {integrity: sha512-9VGjnY23Gc1XryoF/ABWtZVJYnaPOnjHM7dsqq9YALgKRtxI1FryvELUVkDaEIUf4In2bfkb9ZENF1S9M273Dw==} + engines: {node: '>=18.0.0'} '@firebase/app-types@0.9.2': resolution: {integrity: sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ==} - '@firebase/app@0.10.7': - resolution: {integrity: sha512-7OCd53B+wnk/onbMLn/vM10pDjw97zzWUD8m3swtLYKJIrL+gDZ7HZ4xcbBLw7OB8ikzu8k1ORNjRe2itgAy4g==} + '@firebase/app-types@0.9.3': + resolution: {integrity: sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==} + + '@firebase/app@0.13.1': + resolution: {integrity: sha512-0O33PKrXLoIWkoOO5ByFaLjZehBctSYWnb+xJkIdx2SKP/K9l1UPFXPwASyrOIqyY3ws+7orF/1j7wI5EKzPYQ==} + engines: {node: '>=18.0.0'} - '@firebase/auth-compat@0.5.10': - resolution: {integrity: sha512-epDhgNIXmhl9DPuTW9Ec5NDJJKMFIdXBXiQI9O0xNHveow/ETtBCY86srzF7iCacqsd30CcpLwwXlhk8Y19Olg==} + '@firebase/auth-compat@0.5.26': + resolution: {integrity: sha512-4baB7tR0KukyGzrlD25aeO4t0ChLifwvDQXTBiVJE9WWwJEOjkZpHmoU9Iww0+Vdalsq4sZ3abp6YTNjHyB1dA==} + engines: {node: '>=18.0.0'} peerDependencies: '@firebase/app-compat': 0.x - '@firebase/auth-interop-types@0.2.3': - resolution: {integrity: sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ==} + '@firebase/auth-interop-types@0.2.4': + resolution: {integrity: sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==} - '@firebase/auth-types@0.12.2': - resolution: {integrity: sha512-qsEBaRMoGvHO10unlDJhaKSuPn4pyoTtlQuP1ghZfzB6rNQPuhp/N/DcFZxm9i4v0SogjCbf9reWupwIvfmH6w==} + '@firebase/auth-types@0.13.0': + resolution: {integrity: sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==} peerDependencies: '@firebase/app-types': 0.x '@firebase/util': 1.x - '@firebase/auth@1.7.5': - resolution: {integrity: sha512-DMFR1OA/f1/voeuFbSORg9AP36pMgOoSb/DRgiDalLmIJsDTlQNMCu+givjMP4s/XL85+tBk2MerYnK/AscJjw==} + '@firebase/auth@1.10.6': + resolution: {integrity: sha512-cFbo2FymQltog4atI9cKTO6CxKxS0dOMXslTQrlNZRH7qhDG44/d7QeI6GXLweFZtrnlecf52ESnNz1DU6ek8w==} + engines: {node: '>=18.0.0'} peerDependencies: '@firebase/app': 0.x '@react-native-async-storage/async-storage': ^1.18.1 @@ -1940,133 +1978,141 @@ packages: '@react-native-async-storage/async-storage': optional: true - '@firebase/component@0.6.8': - resolution: {integrity: sha512-LcNvxGLLGjBwB0dJUsBGCej2fqAepWyBubs4jt1Tiuns7QLbXHuyObZ4aMeBjZjWx4m8g1LoVI9QFpSaq/k4/g==} + '@firebase/component@0.6.17': + resolution: {integrity: sha512-M6DOg7OySrKEFS8kxA3MU5/xc37fiOpKPMz6cTsMUcsuKB6CiZxxNAvgFta8HGRgEpZbi8WjGIj6Uf+TpOhyzg==} + engines: {node: '>=18.0.0'} + + '@firebase/data-connect@0.3.9': + resolution: {integrity: sha512-B5tGEh5uQrQeH0i7RvlU8kbZrKOJUmoyxVIX4zLA8qQJIN6A7D+kfBlGXtSwbPdrvyaejcRPcbOtqsDQ9HPJKw==} + peerDependencies: + '@firebase/app': 0.x - '@firebase/database-compat@1.0.6': - resolution: {integrity: sha512-1OGA0sLY47mkXjhICCrUTXEYFnSSXoiXWm1SHsN62b+Lzs5aKA3aWTjTUmYIoK93kDAMPkYpulSv8jcbH4Hwew==} + '@firebase/database-compat@2.0.10': + resolution: {integrity: sha512-3sjl6oGaDDYJw/Ny0E5bO6v+KM3KoD4Qo/sAfHGdRFmcJ4QnfxOX9RbG9+ce/evI3m64mkPr24LlmTDduqMpog==} + engines: {node: '>=18.0.0'} - '@firebase/database-types@1.0.4': - resolution: {integrity: sha512-mz9ZzbH6euFXbcBo+enuJ36I5dR5w+enJHHjy9Y5ThCdKUseqfDjW3vCp1YxE9zygFCSjJJ/z1cQ+zodvUcwPQ==} + '@firebase/database-types@1.0.14': + resolution: {integrity: sha512-8a0Q1GrxM0akgF0RiQHliinhmZd+UQPrxEmUv7MnQBYfVFiLtKOgs3g6ghRt/WEGJHyQNslZ+0PocIwNfoDwKw==} - '@firebase/database@1.0.6': - resolution: {integrity: sha512-nrexUEG/fpVlHtWKkyfhTC3834kZ1WS7voNyqbBsBCqHXQOvznN5Z0L3nxBqdXSJyltNAf4ndFlQqm5gZiEczQ==} + '@firebase/database@1.0.19': + resolution: {integrity: sha512-khE+MIYK+XlIndVn/7mAQ9F1fwG5JHrGKaG72hblCC6JAlUBDd3SirICH6SMCf2PQ0iYkruTECth+cRhauacyQ==} + engines: {node: '>=18.0.0'} - '@firebase/firestore-compat@0.3.33': - resolution: {integrity: sha512-i42a2l31N95CwYEB7zmfK0FS1mrO6pwOLwxavCrwu1BCFrVVVQhUheTPIda/iGguK/2Nog0RaIR1bo7QkZEz3g==} + '@firebase/firestore-compat@0.3.52': + resolution: {integrity: sha512-nzt3Sag+EBdm1Jkw/FnnKBPk0LpUUxOlMHMADPBXYhhXrLszxn1+vb64nJsbgRIHfsCn+rg8gyGrb+8frzXrjg==} + engines: {node: '>=18.0.0'} peerDependencies: '@firebase/app-compat': 0.x - '@firebase/firestore-types@3.0.2': - resolution: {integrity: sha512-wp1A+t5rI2Qc/2q7r2ZpjUXkRVPtGMd6zCLsiWurjsQpqPgFin3AhNibKcIzoF2rnToNa/XYtyWXuifjOOwDgg==} + '@firebase/firestore-types@3.0.3': + resolution: {integrity: sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==} peerDependencies: '@firebase/app-types': 0.x '@firebase/util': 1.x - '@firebase/firestore@4.6.4': - resolution: {integrity: sha512-vk2MoH5HxYEhiNg1l+yBXq1Fkhue/11bFg4HdlTv6BJHcTnnAj2a+/afPpatcW4MOdYA3Tv+d5nGzWbbOC1SHw==} - engines: {node: '>=10.10.0'} + '@firebase/firestore@4.7.17': + resolution: {integrity: sha512-YhXWA7HlSnekExhZ5u4i0e+kpPxsh/qMrzeNDgsAva71JXK8OOuOx+yLyYBFhmu3Hr5JJDO2fsZA/wrWoQYHDg==} + engines: {node: '>=18.0.0'} peerDependencies: '@firebase/app': 0.x - '@firebase/functions-compat@0.3.12': - resolution: {integrity: sha512-r3XUb5VlITWpML46JymfJPkK6I9j4SNlO7qWIXUc0TUmkv0oAfVoiIt1F83/NuMZXaGr4YWA/794nVSy4GV8tw==} + '@firebase/functions-compat@0.3.25': + resolution: {integrity: sha512-V0JKUw5W/7aznXf9BQ8LIYHCX6zVCM8Hdw7XUQ/LU1Y9TVP8WKRCnPB/qdPJ0xGjWWn7fhtwIYbgEw/syH4yTQ==} + engines: {node: '>=18.0.0'} peerDependencies: '@firebase/app-compat': 0.x - '@firebase/functions-types@0.6.2': - resolution: {integrity: sha512-0KiJ9lZ28nS2iJJvimpY4nNccV21rkQyor5Iheu/nq8aKXJqtJdeSlZDspjPSBBiHRzo7/GMUttegnsEITqR+w==} + '@firebase/functions-types@0.6.3': + resolution: {integrity: sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==} - '@firebase/functions@0.11.6': - resolution: {integrity: sha512-GPfIBPtpwQvsC7SQbgaUjLTdja0CsNwMoKSgrzA1FGGRk4NX6qO7VQU6XCwBiAFWbpbQex6QWkSMsCzLx1uibQ==} + '@firebase/functions@0.12.8': + resolution: {integrity: sha512-p+ft6dQW0CJ3BLLxeDb5Hwk9ARw01kHTZjLqiUdPRzycR6w7Z75ThkegNmL6gCss3S0JEpldgvehgZ3kHybVhA==} + engines: {node: '>=18.0.0'} peerDependencies: '@firebase/app': 0.x - '@firebase/installations-compat@0.2.8': - resolution: {integrity: sha512-pI2q8JFHB7yIq/szmhzGSWXtOvtzl6tCUmyykv5C8vvfOVJUH6mP4M4iwjbK8S1JotKd/K70+JWyYlxgQ0Kpyw==} + '@firebase/installations-compat@0.2.17': + resolution: {integrity: sha512-J7afeCXB7yq25FrrJAgbx8mn1nG1lZEubOLvYgG7ZHvyoOCK00sis5rj7TgDrLYJgdj/SJiGaO1BD3BAp55TeA==} peerDependencies: '@firebase/app-compat': 0.x - '@firebase/installations-types@0.5.2': - resolution: {integrity: sha512-que84TqGRZJpJKHBlF2pkvc1YcXrtEDOVGiDjovP/a3s6W4nlbohGXEsBJo0JCeeg/UG9A+DEZVDUV9GpklUzA==} + '@firebase/installations-types@0.5.3': + resolution: {integrity: sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==} peerDependencies: '@firebase/app-types': 0.x - '@firebase/installations@0.6.8': - resolution: {integrity: sha512-57V374qdb2+wT5v7+ntpLXBjZkO6WRgmAUbVkRfFTM/4t980p0FesbqTAcOIiM8U866UeuuuF8lYH70D3jM/jQ==} + '@firebase/installations@0.6.17': + resolution: {integrity: sha512-zfhqCNJZRe12KyADtRrtOj+SeSbD1H/K8J24oQAJVv/u02eQajEGlhZtcx9Qk7vhGWF5z9dvIygVDYqLL4o1XQ==} peerDependencies: '@firebase/app': 0.x - '@firebase/logger@0.4.2': - resolution: {integrity: sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A==} + '@firebase/logger@0.4.4': + resolution: {integrity: sha512-mH0PEh1zoXGnaR8gD1DeGeNZtWFKbnz9hDO91dIml3iou1gpOnLqXQ2dJfB71dj6dpmUjcQ6phY3ZZJbjErr9g==} + engines: {node: '>=18.0.0'} - '@firebase/messaging-compat@0.2.10': - resolution: {integrity: sha512-FXQm7rcowkDm8kFLduHV35IRYCRo+Ng0PIp/t1+EBuEbyplaKkGjZ932pE+owf/XR+G/60ku2QRBptRGLXZydg==} + '@firebase/messaging-compat@0.2.21': + resolution: {integrity: sha512-1yMne+4BGLbHbtyu/VyXWcLiefUE1+K3ZGfVTyKM4BH4ZwDFRGoWUGhhx+tKRX4Tu9z7+8JN67SjnwacyNWK5g==} peerDependencies: '@firebase/app-compat': 0.x - '@firebase/messaging-interop-types@0.2.2': - resolution: {integrity: sha512-l68HXbuD2PPzDUOFb3aG+nZj5KA3INcPwlocwLZOzPp9rFM9yeuI9YLl6DQfguTX5eAGxO0doTR+rDLDvQb5tA==} + '@firebase/messaging-interop-types@0.2.3': + resolution: {integrity: sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==} - '@firebase/messaging@0.12.10': - resolution: {integrity: sha512-fGbxJPKpl2DIKNJGhbk4mYPcM+qE2gl91r6xPoiol/mN88F5Ym6UeRdMVZah+pijh9WxM55alTYwXuW40r1Y2Q==} + '@firebase/messaging@0.12.21': + resolution: {integrity: sha512-bYJ2Evj167Z+lJ1ach6UglXz5dUKY1zrJZd15GagBUJSR7d9KfiM1W8dsyL0lDxcmhmA/sLaBYAAhF1uilwN0g==} peerDependencies: '@firebase/app': 0.x - '@firebase/performance-compat@0.2.8': - resolution: {integrity: sha512-o7TFClRVJd3VIBoY7KZQqtCeW0PC6v9uBzM6Lfw3Nc9D7hM6OonqecYvh7NwJ6R14k+xM27frLS4BcCvFHKw2A==} + '@firebase/performance-compat@0.2.19': + resolution: {integrity: sha512-4cU0T0BJ+LZK/E/UwFcvpBCVdkStgBMQwBztM9fJPT6udrEUk3ugF5/HT+E2Z22FCXtIaXDukJbYkE/c3c6IHw==} peerDependencies: '@firebase/app-compat': 0.x - '@firebase/performance-types@0.2.2': - resolution: {integrity: sha512-gVq0/lAClVH5STrIdKnHnCo2UcPLjJlDUoEB/tB4KM+hAeHUxWKnpT0nemUPvxZ5nbdY/pybeyMe8Cs29gEcHA==} + '@firebase/performance-types@0.2.3': + resolution: {integrity: sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==} - '@firebase/performance@0.6.8': - resolution: {integrity: sha512-F+alziiIZ6Yn8FG47mxwljq+4XkgkT2uJIFRlkyViUQRLzrogaUJW6u/+6ZrePXnouKlKIwzqos3PVJraPEcCA==} + '@firebase/performance@0.7.6': + resolution: {integrity: sha512-AsOz74dSTlyQGlnnbLWXiHFAsrxhpssPOsFFi4HgOJ5DjzkK7ZdZ/E9uMPrwFoXJyMVoybGRuqsL/wkIbFITsA==} peerDependencies: '@firebase/app': 0.x - '@firebase/remote-config-compat@0.2.8': - resolution: {integrity: sha512-UxSFOp6dzFj2AHB8Bq/BYtbq5iFyizKx4Rd6WxAdaKYM8cnPMeK+l2v+Oogtjae+AeyHRI+MfL2acsfVe5cd2A==} + '@firebase/remote-config-compat@0.2.17': + resolution: {integrity: sha512-KelsBD0sXSC0u3esr/r6sJYGRN6pzn3bYuI/6pTvvmZbjBlxQkRabHAVH6d+YhLcjUXKIAYIjZszczd1QJtOyA==} peerDependencies: '@firebase/app-compat': 0.x - '@firebase/remote-config-types@0.3.2': - resolution: {integrity: sha512-0BC4+Ud7y2aPTyhXJTMTFfrGGLqdYXrUB9sJVAB8NiqJswDTc4/2qrE/yfUbnQJhbSi6ZaTTBKyG3n1nplssaA==} + '@firebase/remote-config-types@0.4.0': + resolution: {integrity: sha512-7p3mRE/ldCNYt8fmWMQ/MSGRmXYlJ15Rvs9Rk17t8p0WwZDbeK7eRmoI1tvCPaDzn9Oqh+yD6Lw+sGLsLg4kKg==} - '@firebase/remote-config@0.4.8': - resolution: {integrity: sha512-AMLqe6wfIRnjc6FkCWOSUjhc1fSTEf8o+cv1NolFvbiJ/tU+TqN4pI7pT+MIKQzNiq5fxLehkOx+xtAQBxPJKQ==} + '@firebase/remote-config@0.6.4': + resolution: {integrity: sha512-ZyLJRT46wtycyz2+opEkGaoFUOqRQjt/0NX1WfUISOMCI/PuVoyDjqGpq24uK+e8D5NknyTpiXCVq5dowhScmg==} peerDependencies: '@firebase/app': 0.x - '@firebase/storage-compat@0.3.9': - resolution: {integrity: sha512-WWgAp5bTW961oIsCc9+98m4MIVKpEqztAlIngfHfwO/x3DYoBPRl/awMRG3CAXyVxG+7B7oHC5IsnqM+vTwx2A==} + '@firebase/storage-compat@0.3.22': + resolution: {integrity: sha512-29j6JgXTjQ76sOIkxmTNHQfYA/hDTeV9qGbn0jolynPXSg/AmzCB0CpCoCYrS0ja0Flgmy1hkA3XYDZ/eiV1Cg==} + engines: {node: '>=18.0.0'} peerDependencies: '@firebase/app-compat': 0.x - '@firebase/storage-types@0.8.2': - resolution: {integrity: sha512-0vWu99rdey0g53lA7IShoA2Lol1jfnPovzLDUBuon65K7uKG9G+L5uO05brD9pMw+l4HRFw23ah3GwTGpEav6g==} + '@firebase/storage-types@0.8.3': + resolution: {integrity: sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==} peerDependencies: '@firebase/app-types': 0.x '@firebase/util': 1.x - '@firebase/storage@0.12.6': - resolution: {integrity: sha512-Zgb9WuehJxzhj7pGXUvkAEaH+3HvLjD9xSZ9nepuXf5f8378xME7oGJtREr/RnepdDA5YW0XIxe0QQBNHpe1nw==} + '@firebase/storage@0.13.12': + resolution: {integrity: sha512-5JmoFS01MYjW1XMQa5F5rD/kvMwBN10QF03bmcuJWq4lg+BJ3nRgL3sscWnyJPhwM/ZCyv2eRwcfzESVmsYkdQ==} + engines: {node: '>=18.0.0'} peerDependencies: '@firebase/app': 0.x - '@firebase/util@1.9.7': - resolution: {integrity: sha512-fBVNH/8bRbYjqlbIhZ+lBtdAAS4WqZumx03K06/u7fJSpz1TGjEMm1ImvKD47w+xaFKIP2ori6z8BrbakRfjJA==} - - '@firebase/vertexai-preview@0.0.3': - resolution: {integrity: sha512-KVtUWLp+ScgiwkDKAvNkVucAyhLVQp6C6lhnVEuIg4mWhWcS3oerjAeVhZT4uNofKwWxRsOaB2Yec7DMTXlQPQ==} + '@firebase/util@1.12.0': + resolution: {integrity: sha512-Z4rK23xBCwgKDqmzGVMef+Vb4xso2j5Q8OG0vVL4m4fA5ZjPMYQazu8OJJC3vtQRC3SQ/Pgx/6TPNVsCd70QRw==} engines: {node: '>=18.0.0'} - peerDependencies: - '@firebase/app': 0.x - '@firebase/app-types': 0.x - '@firebase/webchannel-wrapper@1.0.1': - resolution: {integrity: sha512-jmEnr/pk0yVkA7mIlHNnxCi+wWzOFUg0WyIotgkKAb2u1J7fAeDBcVNSTjTihbAYNusCLQdW5s9IJ5qwnEufcQ==} + '@firebase/webchannel-wrapper@1.0.3': + resolution: {integrity: sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ==} '@floating-ui/core@1.6.0': resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==} @@ -2121,8 +2167,8 @@ packages: '@fullcalendar/core': ~6.1.11 vue: ^3.0.11 - '@google-cloud/firestore@7.9.0': - resolution: {integrity: sha512-c4ALHT3G08rV7Zwv8Z2KG63gZh66iKdhCBeDfCpIkLrjX6EAjTD/szMdj14M+FnQuClZLFfW5bAgoOjfNmLtJg==} + '@google-cloud/firestore@7.11.1': + resolution: {integrity: sha512-ZxOdH8Wr01hBDvKCQfMWqwUcfNcN3JY19k1LtS1fTFhEyorYPLsbWN+VxIRL46pOYGHTPkU3Or5HbT/SLQM5nA==} engines: {node: '>=14.0.0'} '@google-cloud/paginator@5.0.2': @@ -2137,8 +2183,8 @@ packages: resolution: {integrity: sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==} engines: {node: '>=14'} - '@google-cloud/storage@7.12.0': - resolution: {integrity: sha512-122Ui67bhnf8MkRnxQAC5lf7wPGkPP5hL3+J5s9HHDw2J9RpaMmnV8iahn+RUn9BH70W6uRe6nMZLXiRaJM/3g==} + '@google-cloud/storage@7.16.0': + resolution: {integrity: sha512-7/5LRgykyOfQENcm6hDKP8SX/u9XxE5YOiWOkgkwcoO+cG8xT/cyOvp9wwN3IxfdYgpHs8CE7Nq2PKX2lNaEXw==} engines: {node: '>=14'} '@grpc/grpc-js@1.11.1': @@ -2154,9 +2200,6 @@ packages: engines: {node: '>=6'} hasBin: true - '@hotjar/browser@1.0.9': - resolution: {integrity: sha512-n9akDMod8BLGpYEQCrHwlYWWd63c1HlhUSXNIDfClZtKYXbUjIUOFlNZNNcUxgHTCsi4l2i+SWKsGsO0t93S8w==} - '@humanwhocodes/config-array@0.11.14': resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} @@ -2351,6 +2394,9 @@ packages: peerDependencies: rollup: ^1.20.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 + '@mongodb-js/saslprep@1.2.2': + resolution: {integrity: sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==} + '@netlify/functions@2.8.1': resolution: {integrity: sha512-+6wtYdoz0yE06dSa9XkP47tw5zm6g13QMeCwM3MmHx1vn8hzwFa51JtmfraprdkL7amvb7gaNM+OOhQU1h6T8A==} engines: {node: '>=14.0.0'} @@ -2765,6 +2811,9 @@ packages: '@payos/node@1.0.10': resolution: {integrity: sha512-dY+WHd6pLa558a1G8yv6oKfVe5QLTNyYnQBaSQtwvMAm/p0faKAnfXt04LNIwO9/4buas4ES+sDxc1bfX/mVbQ==} + '@petamoriken/float16@3.9.2': + resolution: {integrity: sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog==} + '@pinia/nuxt@0.5.1': resolution: {integrity: sha512-6wT6TqY81n+7/x3Yhf0yfaJVKkZU42AGqOR0T3+UvChcaOJhSma7OWPN64v+ptYlznat+fS1VTwNAcbi2lzHnw==} @@ -2828,11 +2877,6 @@ packages: '@protobufjs/utf8@1.1.0': resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} - '@puppeteer/browsers@2.4.0': - resolution: {integrity: sha512-x8J1csfIygOwf6D6qUAZ0ASk3z63zPb7wkNeHRerCMh82qWKUrOgkuP005AJC8lDL6/evtXETGEJVcwykKT4/g==} - engines: {node: '>=18'} - hasBin: true - '@remirror/core-constants@2.0.2': resolution: {integrity: sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ==} @@ -3373,18 +3417,6 @@ packages: peerDependencies: eslint: '>=8.40.0' - '@stylistic/stylelint-config@1.0.1': - resolution: {integrity: sha512-JgFP88HZEyo34k9RpWVdcQJtLPrMxYE58IO3qypXhmvE/NmZohj+xjDtQ8UfaarnYsLecnldw57/GHum07Ctdw==} - engines: {node: ^18.12 || >=20.9} - peerDependencies: - stylelint: ^16.0.2 - - '@stylistic/stylelint-plugin@2.1.1': - resolution: {integrity: sha512-xqHTmQZN7EbnFDW7jw0rAsdFNO4IRqvXhrh3qhUlIwF/x09Zm7kgs/ADktHxsTJYcw346PpGihsB0t4pZhpeHw==} - engines: {node: ^18.12 || >=20.9} - peerDependencies: - stylelint: ^16.0.2 - '@swc/helpers@0.5.13': resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==} @@ -3565,9 +3597,6 @@ packages: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} - '@tootallnate/quickjs-emscripten@0.23.0': - resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} - '@trysound/sax@0.2.0': resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} @@ -3644,6 +3673,9 @@ packages: '@types/node@20.12.7': resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==} + '@types/node@22.15.30': + resolution: {integrity: sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==} + '@types/nodemailer@6.4.16': resolution: {integrity: sha512-uz6hN6Pp0upXMcilM61CoKyjT7sskBoOWpptkjjJp8jIMlTdc3xG01U7proKkXzruMS4hS0zqtHNkNPFB20rKQ==} @@ -3695,6 +3727,9 @@ packages: '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + '@types/unist@2.0.10': resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} @@ -3710,6 +3745,12 @@ packages: '@types/webfontloader@1.6.38': resolution: {integrity: sha512-kUaF72Fv202suFx6yBrwXqeVRMx7hGtJTesyESZgn9sEPCUeDXm2p0SiyS1MTqW74nQP4p7JyrOCwZ7pNFns4w==} + '@types/webidl-conversions@7.0.3': + resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} + + '@types/whatwg-url@11.0.5': + resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==} + '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -4002,6 +4043,9 @@ packages: peerDependencies: vue: '>=2.7 || >=3' + '@upstash/redis@1.35.0': + resolution: {integrity: sha512-WUm0Jz1xN4DBDGeJIi2Y0kVsolWRB2tsVds4SExaiLg4wBdHFMB+8IfZtBWr+BP0FvhuBr5G1/VLrJ9xzIWHsg==} + '@vercel/nft@0.26.5': resolution: {integrity: sha512-NHxohEqad6Ra/r4lGknO52uc/GrWILXAMs1BB4401GTqww0fw1bAqzpG1XHuDO+dprg4GvsD9ZLLSsdo78p9hQ==} engines: {node: '>=16'} @@ -4329,10 +4373,6 @@ packages: resolution: {integrity: sha512-7TnogTQQZEagrHcOcddY0PqXPxVqFoNPPsKoa42Peyc83iinzT+QPKoRLDmzpaUVWZbgqSoHtezsTIoJyyBE+Q==} engines: {node: '>=16.14.0'} - ast-types@0.13.4: - resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} - engines: {node: '>=4'} - ast-walker-scope@0.6.2: resolution: {integrity: sha512-1UWOyC50xI3QZkRuDj6PqDtpm1oHWtYs+NQGwqL/2R11eN3Q81PHAHPM0SWW3BNQm53UDwS//Jv8L4CCVLM1bQ==} engines: {node: '>=16.14.0'} @@ -4375,18 +4415,6 @@ packages: bare-events@2.2.2: resolution: {integrity: sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==} - bare-fs@2.3.5: - resolution: {integrity: sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==} - - bare-os@2.4.4: - resolution: {integrity: sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ==} - - bare-path@2.1.3: - resolution: {integrity: sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==} - - bare-stream@2.3.0: - resolution: {integrity: sha512-pVRWciewGUeCyKEuRxwv06M079r+fRjAQjBEK2P6OYGrO43O+Z0LrPZZEjlc4mB6C2RpZ9AxJ1s7NLEtOHO6eA==} - base64-js@1.3.1: resolution: {integrity: sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==} @@ -4397,10 +4425,6 @@ packages: resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} engines: {node: '>= 0.8'} - basic-ftp@5.0.5: - resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} - engines: {node: '>=10.0.0'} - bcrypt@5.1.1: resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==} engines: {node: '>= 10.0.0'} @@ -4454,6 +4478,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bson@6.10.4: + resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==} + engines: {node: '>=16.20.1'} + buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} @@ -4467,9 +4495,6 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -4597,15 +4622,14 @@ packages: resolution: {integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==} engines: {node: '>= 14.16.0'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + chownr@2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} - chromium-bidi@0.6.5: - resolution: {integrity: sha512-RuLrmzYrxSb0s9SgpB+QN5jJucPduZQ/9SIe76MDxYJuecPW5mxMdacJ1f4EtgiV+R0p3sCkznTMvH0MPGFqjA==} - peerDependencies: - devtools-protocol: '*' - ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -4660,13 +4684,22 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + color-support@1.1.3: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true + color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + colorspace@1.1.4: + resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -4804,6 +4837,10 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + crossws@0.2.4: resolution: {integrity: sha512-DAxroI2uSOgUKLz00NX6A8U/8EE3SZHmIND+10jkVSaypvyt57J5JEOxAQOL6lQxyzi/wZbTIwssU1uy69h5Vg==} peerDependencies: @@ -4884,10 +4921,6 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - data-uri-to-buffer@6.0.2: - resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} - engines: {node: '>= 14'} - date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} @@ -4972,10 +5005,6 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} - degenerator@5.0.1: - resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} - engines: {node: '>= 14'} - delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -4994,6 +5023,9 @@ packages: destr@2.0.3: resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -5010,9 +5042,6 @@ packages: devalue@5.0.0: resolution: {integrity: sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA==} - devtools-protocol@0.0.1342118: - resolution: {integrity: sha512-75fMas7PkYNDTmDyb6PRJCH7ILmHLp+BhrZGeMsa4bCh40DTxgCz2NRy5UDzII4C5KuD0oBMZ9vXKhEl6UD/3w==} - dfa@1.2.0: resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} @@ -5064,18 +5093,19 @@ packages: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} - drizzle-kit@0.23.2: - resolution: {integrity: sha512-NWkQ7GD2OTbQ7HzcjsaCOf3n0tlFPSEAF38fvDpwDj8jRbGWGFtN2cD8I8wp4lU+5Os/oyP2xycTKGLHdPipUw==} + drizzle-kit@0.30.6: + resolution: {integrity: sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g==} hasBin: true - drizzle-orm@0.32.2: - resolution: {integrity: sha512-3fXKzPzrgZIcnWCSLiERKN5Opf9Iagrag75snfFlKeKSYB1nlgPBshzW3Zn6dQymkyiib+xc4nIz0t8U+Xdpuw==} + drizzle-orm@0.40.1: + resolution: {integrity: sha512-aPNhtiJiPfm3qxz1czrnIDkfvkSdKGXYeZkpG55NPTVI186LmK2fBLMi4dsHpPHlJrZeQ92D322YFPHADBALew==} peerDependencies: '@aws-sdk/client-rds-data': '>=3' - '@cloudflare/workers-types': '>=3' - '@electric-sql/pglite': '>=0.1.1' - '@libsql/client': '*' - '@neondatabase/serverless': '>=0.1' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' '@op-engineering/op-sqlite': '>=2' '@opentelemetry/api': ^1.4.1 '@planetscale/database': '>=1' @@ -5083,20 +5113,19 @@ packages: '@tidbcloud/serverless': '*' '@types/better-sqlite3': '*' '@types/pg': '*' - '@types/react': '>=18' '@types/sql.js': '*' '@vercel/postgres': '>=0.8.0' '@xata.io/client': '*' better-sqlite3: '>=7' bun-types: '*' - expo-sqlite: '>=13.2.0' + expo-sqlite: '>=14.0.0' + gel: '>=2' knex: '*' kysely: '*' mysql2: '>=2' pg: '>=8' postgres: '>=3' prisma: '*' - react: '>=18' sql.js: '>=1' sqlite3: '>=5' peerDependenciesMeta: @@ -5108,6 +5137,8 @@ packages: optional: true '@libsql/client': optional: true + '@libsql/client-wasm': + optional: true '@neondatabase/serverless': optional: true '@op-engineering/op-sqlite': @@ -5124,8 +5155,6 @@ packages: optional: true '@types/pg': optional: true - '@types/react': - optional: true '@types/sql.js': optional: true '@vercel/postgres': @@ -5138,6 +5167,8 @@ packages: optional: true expo-sqlite: optional: true + gel: + optional: true knex: optional: true kysely: @@ -5150,18 +5181,16 @@ packages: optional: true prisma: optional: true - react: - optional: true sql.js: optional: true sqlite3: optional: true - drizzle-zod@0.5.1: - resolution: {integrity: sha512-C/8bvzUH/zSnVfwdSibOgFjLhtDtbKYmkbPbUCq46QZyZCH6kODIMSOgZ8R7rVjoI+tCj3k06MRJMDqsIeoS4A==} + drizzle-zod@0.7.1: + resolution: {integrity: sha512-nZzALOdz44/AL2U005UlmMqaQ1qe5JfanvLujiTHiiT8+vZJTBFhj3pY4Vk+L6UWyKFfNmLhk602Hn4kCTynKQ==} peerDependencies: - drizzle-orm: '>=0.23.13' - zod: '*' + drizzle-orm: '>=0.36.0' + zod: '>=3.0.0' duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -5193,6 +5222,9 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} @@ -5215,6 +5247,10 @@ packages: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} + env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} @@ -5663,8 +5699,8 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} - execa@9.3.0: - resolution: {integrity: sha512-l6JFbqnHEadBoVAVpN5dl2yCyfX28WoBAGaoQcNmLLSedOxTxcn2Qa83s8I/PA5i56vWru2OHOtrwF7Om2vqlg==} + execa@9.6.0: + resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==} engines: {node: ^18.19.0 || >=20.5.0} express@4.19.2: @@ -5750,6 +5786,9 @@ packages: picomatch: optional: true + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -5762,6 +5801,9 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-stream-rotator@0.6.1: + resolution: {integrity: sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==} + file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -5789,9 +5831,9 @@ packages: resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} engines: {node: '>=18'} - firebase-admin@12.2.0: - resolution: {integrity: sha512-R9xxENvPA/19XJ3mv0Kxfbz9kPXd9/HrM4083LZWOO0qAQGheRzcCQamYRe+JSrV2cdKXP3ZsfFGTYMrFM0pJg==} - engines: {node: '>=14'} + firebase-admin@13.4.0: + resolution: {integrity: sha512-Y8DcyKK+4pl4B93ooiy1G8qvdyRMkcNFfBSh+8rbVcw4cW8dgG0VXCCTp5NUwub8sn9vSPsOwpb9tE2OuFmcfQ==} + engines: {node: '>=18'} firebase-functions@5.0.1: resolution: {integrity: sha512-1m+crtgAR8Tl36gjpM02KCY5zduAejFmDSXvih/DB93apg39f0U/WwRgT7sitGIRqyCcIpktNUbXJv7Y9JOF4A==} @@ -5800,8 +5842,8 @@ packages: peerDependencies: firebase-admin: ^11.10.0 || ^12.0.0 - firebase@10.12.4: - resolution: {integrity: sha512-SQz49NMpwG4MLTPZ9C8jBp7IyS2haTvsIvjclgu+v/jvzNtjZoxIcoF6A13EIfBHmJ5eiuVlvttxElOf7LnJew==} + firebase@11.9.0: + resolution: {integrity: sha512-7uIGhxKtTNfDcoMKWn0G8G0Z1Zj5VeW8uzImAcUmI31PaYQdVWi2rVVig7thWB3vPianESPrLEKim2Fw7U8fiA==} flat-cache@3.2.0: resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} @@ -5817,6 +5859,9 @@ packages: flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + follow-redirects@1.15.6: resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} engines: {node: '>=4.0'} @@ -5893,6 +5938,11 @@ packages: resolution: {integrity: sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==} engines: {node: '>=14'} + gel@2.1.0: + resolution: {integrity: sha512-HCeRqInCt6BjbMmeghJ6BKeYwOj7WJT5Db6IWWAA3IMUUa7or7zJfTUEkUWCxiOtoXnwnm96sFK9Fr47Yh2hOA==} + engines: {node: '>= 18.0.0'} + hasBin: true + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -5933,10 +5983,6 @@ packages: get-tsconfig@4.8.1: resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} - get-uri@6.0.3: - resolution: {integrity: sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==} - engines: {node: '>= 14'} - giget@1.2.3: resolution: {integrity: sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==} hasBin: true @@ -6031,8 +6077,8 @@ packages: globjoin@0.1.4: resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} - google-auth-library@9.11.0: - resolution: {integrity: sha512-epX3ww/mNnhl6tL45EQ/oixsY8JLEgUFoT4A5E/5iAR4esld9Kqv6IJGk7EmGuOgDvaarwF95hU2+v7Irql9lw==} + google-auth-library@9.15.1: + resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} engines: {node: '>=14'} google-gax@4.3.8: @@ -6122,10 +6168,6 @@ packages: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} - http-proxy-agent@7.0.2: - resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} - engines: {node: '>= 14'} - http-shutdown@1.2.2: resolution: {integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -6153,8 +6195,8 @@ packages: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} - human-signals@7.0.0: - resolution: {integrity: sha512-74kytxOUSvNbjrT9KisAbaTZ/eJwD/LrbM/kh5j0IhPuJzwuA19dWvniFGwBzN9rVjg+O/e+F310PjObDXS+9Q==} + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} engines: {node: '>=18.18.0'} iconv-lite@0.4.24: @@ -6230,14 +6272,10 @@ packages: resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - ioredis@5.4.1: - resolution: {integrity: sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==} + ioredis@5.6.1: + resolution: {integrity: sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==} engines: {node: '>=12.22.0'} - ip-address@9.0.5: - resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} - engines: {node: '>= 12'} - ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -6258,6 +6296,9 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -6391,6 +6432,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isexe@3.1.1: + resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} + engines: {node: '>=16'} + jackspeak@2.3.6: resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} engines: {node: '>=14'} @@ -6407,10 +6452,6 @@ packages: resolution: {integrity: sha512-pmfRbVRs/7khFrSAYnSiJ8C0D5GvzkE4Ey2pAvUcJsw1ly/p+7ut27jbJrjY79BpAJQJ4gXYFtK6d1Aub+9baQ==} hasBin: true - jiti@2.4.0: - resolution: {integrity: sha512-H5UpaUI+aHOqZXlYOaFP/8AzKsg+guWu+Pr3Y8i7+Y3zr1aXAvCvTAQ1RxSc6oVD8R8c7brgNtTVP91E7upH/g==} - hasBin: true - jiti@2.4.2: resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true @@ -6440,9 +6481,6 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true - jsbn@1.1.0: - resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} - jsdoc-type-pratt-parser@4.0.0: resolution: {integrity: sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==} engines: {node: '>=12.0.0'} @@ -6554,6 +6592,9 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + launch-editor@2.9.1: resolution: {integrity: sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==} @@ -6671,13 +6712,13 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + long@5.2.3: resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} - loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true - loupe@3.1.1: resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} @@ -6695,10 +6736,6 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} - lru-cache@7.18.3: - resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} - engines: {node: '>=12'} - lru-memoizer@2.3.0: resolution: {integrity: sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==} @@ -6775,6 +6812,9 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + memory-pager@1.5.0: + resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} + meow@13.2.0: resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} engines: {node: '>=18'} @@ -6925,6 +6965,36 @@ packages: moment@2.30.1: resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + mongodb-connection-string-url@3.0.2: + resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==} + + mongodb@6.17.0: + resolution: {integrity: sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA==} + engines: {node: '>=16.20.1'} + peerDependencies: + '@aws-sdk/credential-providers': ^3.188.0 + '@mongodb-js/zstd': ^1.1.0 || ^2.0.0 + gcp-metadata: ^5.2.0 + kerberos: ^2.0.1 + mongodb-client-encryption: '>=6.0.0 <7' + snappy: ^7.2.2 + socks: ^2.7.1 + peerDependenciesMeta: + '@aws-sdk/credential-providers': + optional: true + '@mongodb-js/zstd': + optional: true + gcp-metadata: + optional: true + kerberos: + optional: true + mongodb-client-encryption: + optional: true + snappy: + optional: true + socks: + optional: true + mpd-parser@1.3.0: resolution: {integrity: sha512-WgeIwxAqkmb9uTn4ClicXpEQYCEduDqRKfmUdp4X8vmghKfBNXZLYpREn9eqrDx/Tf5LhzRcJLSpi4ohfV742Q==} hasBin: true @@ -6964,8 +7034,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@5.0.7: - resolution: {integrity: sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==} + nanoid@5.1.5: + resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==} engines: {node: ^18 || >=20} hasBin: true @@ -6982,10 +7052,6 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} - netmask@2.0.2: - resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} - engines: {node: '>= 0.4.0'} - nitropack@2.9.7: resolution: {integrity: sha512-aKXvtNrWkOCMsQbsk4A0qQdBjrJ1ZcvwlTQevI/LAgLWLYc5L7Q/YiYxGLal4ITyNSlzir1Cm1D2ZxnYhmpMEw==} engines: {node: ^16.11.0 || >=17.0.0} @@ -7006,6 +7072,9 @@ packages: node-fetch-native@1.6.4: resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} + node-fetch-native@1.6.6: + resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -7026,8 +7095,8 @@ packages: node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} - nodemailer@6.9.15: - resolution: {integrity: sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==} + nodemailer@6.10.1: + resolution: {integrity: sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==} engines: {node: '>=6.0.0'} nopt@5.0.0: @@ -7054,6 +7123,10 @@ packages: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + npmlog@5.0.1: resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} deprecated: This package is no longer supported. @@ -7072,9 +7145,6 @@ packages: nuxt-gtag@3.0.2: resolution: {integrity: sha512-0Spu/rffPxW7REebkjE22qQOuVZIQuyIuvd61InDdJU+d+gRIqWhrUuKBWdk43N/J1elj+cTK2kEdk5FQX7vdQ==} - nuxt-module-hotjar@1.3.2: - resolution: {integrity: sha512-4jRp5LIb8aS5jh+Mk6ihHfQ92bLWWtKuFmWUVeczM93gjRSObSkyi8w9XGRaceYH5+vVxAI02tVQ3/gT0M2XFA==} - nuxt-nodemailer@1.1.2: resolution: {integrity: sha512-oYi0AnEZu56euRxJ/G7NAQLmRct9ORgKvxF3AcAbVlxfKJJQ7kNPZnlymQbzg9F/SLBlQgSE+mSQR/3Wz6JOIg==} peerDependencies: @@ -7143,6 +7213,9 @@ packages: ofetch@1.3.4: resolution: {integrity: sha512-KLIET85ik3vhEfS+3fDlc/BAZiAp+43QEC/yCo5zkNoY2YaKvNkOaFr/6wCFgFH1kuYQM5pMNi0Tg8koiIemtw==} + ofetch@1.4.1: + resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} + ohash@1.1.4: resolution: {integrity: sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==} @@ -7153,6 +7226,9 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -7208,14 +7284,6 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - pac-proxy-agent@7.0.2: - resolution: {integrity: sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==} - engines: {node: '>= 14'} - - pac-resolver@7.0.1: - resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} - engines: {node: '>= 14'} - package-json-from-dist@1.0.0: resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} @@ -7703,8 +7771,8 @@ packages: resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} engines: {node: ^14.13.1 || >=16.0.0} - pretty-ms@9.1.0: - resolution: {integrity: sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==} + pretty-ms@9.2.0: + resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==} engines: {node: '>=18'} prismjs@1.29.0: @@ -7802,10 +7870,6 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} - proxy-agent@6.4.0: - resolution: {integrity: sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==} - engines: {node: '>= 14'} - proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -7820,15 +7884,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - puppeteer-core@23.4.1: - resolution: {integrity: sha512-uCxGtn8VE9PlKhdFJX/zZySi9K3Ufr3qUZe28jxJoZUqiMJOi+SFh2zhiFDSjWqZIDkc0FtnaCC+rewW3MYXmg==} - engines: {node: '>=18'} - - puppeteer@23.4.1: - resolution: {integrity: sha512-+wWfWTkQ8L9IB/3OVGSUp37c0eQ5za/85KdX+LAq2wTZkMdocgYGMCs+/91e2f/RXIYzve4x/uGxN8zG2sj8+w==} - engines: {node: '>=18'} - hasBin: true - qs@6.11.0: resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} engines: {node: '>=0.6'} @@ -7866,10 +7921,6 @@ packages: rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} - react@18.2.0: - resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} - engines: {node: '>=0.10.0'} - read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} engines: {node: '>=8'} @@ -8044,12 +8095,13 @@ packages: safe-json-parse@4.0.0: resolution: {integrity: sha512-RjZPPHugjK0TOzFrLZ8inw44s9bKox99/0AZW9o/BEQVrJfhI+fIHMErnPyRa89/yRXUUr93q+tiN6zhoVV4wQ==} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - safevalues@0.6.0: - resolution: {integrity: sha512-MZ7DcTOcIoPXN36/UONVE9BT0pmwlCr9WcS7Pj/q4FxOwr33FkWC0CUWj/THQXYWxf/F7urbhaHaOeFPSqGqHA==} - sass@1.75.0: resolution: {integrity: sha512-ShMYi3WkrDWxExyxSZPst4/okE9ts46xZmJDSawJQrnte7M1V9fScVB+uNXOVKRBt0PggHOwoZcn8mYX4trnBw==} engines: {node: '>=14.0.0'} @@ -8151,6 +8203,9 @@ packages: simple-git@3.27.0: resolution: {integrity: sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==} + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + sirv@2.0.4: resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} @@ -8177,21 +8232,9 @@ packages: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} - smart-buffer@4.2.0: - resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} - engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - smob@1.5.0: resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==} - socks-proxy-agent@8.0.4: - resolution: {integrity: sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==} - engines: {node: '>= 14'} - - socks@2.8.3: - resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==} - engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} - source-map-js@1.2.0: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} @@ -8207,6 +8250,9 @@ packages: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} + sparse-bitfield@3.0.3: + resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} + spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -8230,12 +8276,12 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} - sprintf-js@1.1.3: - resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} - stable-hash@0.0.4: resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==} + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -8327,9 +8373,6 @@ packages: stubs@3.0.0: resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} - style-search@0.1.0: - resolution: {integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==} - stylehacks@7.0.4: resolution: {integrity: sha512-i4zfNrGMt9SB4xRK9L83rlsFCgdGANfeDAYacO1pkqcE7cRHPdWHwnKZVz7WY17Veq/FvyYsRAU++Ga+qDFIww==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} @@ -8491,9 +8534,6 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} - tar-fs@3.0.6: - resolution: {integrity: sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==} - tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} @@ -8513,6 +8553,9 @@ packages: text-decoder@1.2.0: resolution: {integrity: sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg==} + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -8523,9 +8566,6 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - through@2.3.8: - resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} @@ -8595,6 +8635,14 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + ts-api-utils@1.3.0: resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} @@ -8671,9 +8719,6 @@ packages: type-level-regexp@0.1.17: resolution: {integrity: sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==} - typed-query-selector@2.12.0: - resolution: {integrity: sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==} - typescript@5.4.5: resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} engines: {node: '>=14.17'} @@ -8688,6 +8733,9 @@ packages: ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + ultrahtml@1.5.3: resolution: {integrity: sha512-GykOvZwgDWZlTQMtp5jrD4BVL+gNn2NVlVafjcFUJ7taY20tqYdwdoWBFy6GBJsNTZe1GkGPkSl5knQAjtgceg==} @@ -8700,9 +8748,6 @@ packages: typescript: optional: true - unbzip2-stream@1.4.3: - resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} - unconfig@0.5.5: resolution: {integrity: sha512-VQZ5PT9HDX+qag0XdgQi8tJepPhXiR/yVOkn707gJDKo31lGjRilPREiQJ9Z6zd/Ugpv6ZvO5VxVIcatldYcNQ==} @@ -8721,6 +8766,9 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@5.28.4: resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} engines: {node: '>=14.0'} @@ -8741,6 +8789,10 @@ packages: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + unimport@3.12.0: resolution: {integrity: sha512-5y8dSvNvyevsnw4TBQkIQR1Rjdbb+XjVSwQwxltpnVZrStBvvPkMPcZrh1kg5kY77kpx6+D4Ztd3W6FOBH/y2Q==} @@ -8801,22 +8853,27 @@ packages: resolution: {integrity: sha512-m1ekpSwuOT5hxkJeZGRxO7gXbXT3gF26NjQ7GdVHoLoF8/nopLcd/QfPigpCy7i51oFHiRJg/CyHhj4vs2+KGw==} engines: {node: '>=18.12.0'} - unstorage@1.12.0: - resolution: {integrity: sha512-ARZYTXiC+e8z3lRM7/qY9oyaOkaozCeNd2xoz7sYK9fv7OLGhVsf+BZbmASqiK/HTZ7T6eAlnVq9JynZppyk3w==} - peerDependencies: - '@azure/app-configuration': ^1.7.0 - '@azure/cosmos': ^4.1.1 - '@azure/data-tables': ^13.2.2 - '@azure/identity': ^4.4.1 - '@azure/keyvault-secrets': ^4.8.0 - '@azure/storage-blob': ^12.24.0 - '@capacitor/preferences': ^6.0.2 - '@netlify/blobs': ^6.5.0 || ^7.0.0 + unstorage@1.16.0: + resolution: {integrity: sha512-WQ37/H5A7LcRPWfYOrDa1Ys02xAbpPJq6q5GkO88FBXVSQzHd7+BjEwfRqyaSWCv9MbsJy058GWjjPjcJ16GGA==} + peerDependencies: + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6.0.3 || ^7.0.0 + '@deno/kv': '>=0.9.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 '@planetscale/database': ^1.19.0 - '@upstash/redis': ^1.34.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' '@vercel/kv': ^1.0.1 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' idb-keyval: ^6.2.1 - ioredis: ^5.4.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 peerDependenciesMeta: '@azure/app-configuration': optional: true @@ -8832,18 +8889,28 @@ packages: optional: true '@capacitor/preferences': optional: true + '@deno/kv': + optional: true '@netlify/blobs': optional: true '@planetscale/database': optional: true '@upstash/redis': optional: true + '@vercel/blob': + optional: true '@vercel/kv': optional: true + aws4fetch: + optional: true + db0: + optional: true idb-keyval: optional: true ioredis: optional: true + uploadthing: + optional: true untun@0.1.3: resolution: {integrity: sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ==} @@ -8889,9 +8956,6 @@ packages: url-toolkit@2.2.5: resolution: {integrity: sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==} - urlpattern-polyfill@10.0.0: - resolution: {integrity: sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==} - urlpattern-polyfill@8.0.2: resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==} @@ -8906,6 +8970,10 @@ packages: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -9269,9 +9337,16 @@ packages: w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + web-vitals@4.2.4: + resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + webpack-sources@3.2.3: resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} engines: {node: '>=10.13.0'} @@ -9290,6 +9365,10 @@ packages: resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} engines: {node: '>=0.8.0'} + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -9307,6 +9386,11 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} hasBin: true + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -9315,6 +9399,20 @@ packages: wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + winston-daily-rotate-file@5.0.0: + resolution: {integrity: sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==} + engines: {node: '>=8'} + peerDependencies: + winston: ^3 + + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.17.0: + resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==} + engines: {node: '>= 12.0.0'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -9484,7 +9582,7 @@ snapshots: - supports-color - typescript - '@antfu/eslint-config@2.22.0(@vue/compiler-sfc@3.5.8)(eslint@8.57.0)(typescript@5.4.5)(vitest@2.1.1(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))': + '@antfu/eslint-config@2.22.0(@vue/compiler-sfc@3.5.8)(eslint@8.57.0)(typescript@5.4.5)(vitest@2.1.1(@types/node@22.15.30)(sass@1.75.0)(terser@5.30.4))': dependencies: '@antfu/install-pkg': 0.3.3 '@clack/prompts': 0.7.0 @@ -9509,7 +9607,7 @@ snapshots: eslint-plugin-toml: 0.11.1(eslint@8.57.0) eslint-plugin-unicorn: 54.0.0(eslint@8.57.0) eslint-plugin-unused-imports: 4.0.0(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0) - eslint-plugin-vitest: 0.5.4(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)(vitest@2.1.1(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4)) + eslint-plugin-vitest: 0.5.4(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)(vitest@2.1.1(@types/node@22.15.30)(sass@1.75.0)(terser@5.30.4)) eslint-plugin-vue: 9.28.0(eslint@8.57.0) eslint-plugin-yml: 1.14.0(eslint@8.57.0) eslint-processor-vue-blocks: 0.1.2(@vue/compiler-sfc@3.5.8)(eslint@8.57.0) @@ -10432,6 +10530,8 @@ snapshots: dependencies: mime: 3.0.0 + '@colors/colors@1.6.0': {} + '@csstools/css-parser-algorithms@2.6.1(@csstools/css-tokenizer@2.2.4)': dependencies: '@csstools/css-tokenizer': 2.2.4 @@ -10447,7 +10547,13 @@ snapshots: dependencies: postcss-selector-parser: 6.1.2 - '@drizzle-team/brocli@0.8.2': {} + '@dabh/diagnostics@2.0.3': + dependencies: + colorspace: 1.1.4 + enabled: 2.0.0 + kuler: 2.0.0 + + '@drizzle-team/brocli@0.10.2': {} '@es-joy/jsdoccomment@0.41.0': dependencies: @@ -10981,320 +11087,327 @@ snapshots: '@fastify/busboy@2.1.1': {} - '@firebase/analytics-compat@0.2.12(@firebase/app-compat@0.2.37)(@firebase/app@0.10.7)': + '@fastify/busboy@3.1.1': {} + + '@firebase/ai@1.4.0(@firebase/app-types@0.9.3)(@firebase/app@0.13.1)': dependencies: - '@firebase/analytics': 0.10.6(@firebase/app@0.10.7) - '@firebase/analytics-types': 0.8.2 - '@firebase/app-compat': 0.2.37 - '@firebase/component': 0.6.8 - '@firebase/util': 1.9.7 + '@firebase/app': 0.13.1 + '@firebase/app-check-interop-types': 0.3.3 + '@firebase/app-types': 0.9.3 + '@firebase/component': 0.6.17 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 + tslib: 2.7.0 + + '@firebase/analytics-compat@0.2.22(@firebase/app-compat@0.4.1)(@firebase/app@0.13.1)': + dependencies: + '@firebase/analytics': 0.10.16(@firebase/app@0.13.1) + '@firebase/analytics-types': 0.8.3 + '@firebase/app-compat': 0.4.1 + '@firebase/component': 0.6.17 + '@firebase/util': 1.12.0 tslib: 2.7.0 transitivePeerDependencies: - '@firebase/app' - '@firebase/analytics-types@0.8.2': {} + '@firebase/analytics-types@0.8.3': {} - '@firebase/analytics@0.10.6(@firebase/app@0.10.7)': + '@firebase/analytics@0.10.16(@firebase/app@0.13.1)': dependencies: - '@firebase/app': 0.10.7 - '@firebase/component': 0.6.8 - '@firebase/installations': 0.6.8(@firebase/app@0.10.7) - '@firebase/logger': 0.4.2 - '@firebase/util': 1.9.7 - safevalues: 0.6.0 + '@firebase/app': 0.13.1 + '@firebase/component': 0.6.17 + '@firebase/installations': 0.6.17(@firebase/app@0.13.1) + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 tslib: 2.7.0 - '@firebase/app-check-compat@0.3.13(@firebase/app-compat@0.2.37)(@firebase/app@0.10.7)': + '@firebase/app-check-compat@0.3.25(@firebase/app-compat@0.4.1)(@firebase/app@0.13.1)': dependencies: - '@firebase/app-check': 0.8.6(@firebase/app@0.10.7) - '@firebase/app-check-types': 0.5.2 - '@firebase/app-compat': 0.2.37 - '@firebase/component': 0.6.8 - '@firebase/logger': 0.4.2 - '@firebase/util': 1.9.7 + '@firebase/app-check': 0.10.0(@firebase/app@0.13.1) + '@firebase/app-check-types': 0.5.3 + '@firebase/app-compat': 0.4.1 + '@firebase/component': 0.6.17 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 tslib: 2.7.0 transitivePeerDependencies: - '@firebase/app' - '@firebase/app-check-interop-types@0.3.2': {} + '@firebase/app-check-interop-types@0.3.3': {} - '@firebase/app-check-types@0.5.2': {} + '@firebase/app-check-types@0.5.3': {} - '@firebase/app-check@0.8.6(@firebase/app@0.10.7)': + '@firebase/app-check@0.10.0(@firebase/app@0.13.1)': dependencies: - '@firebase/app': 0.10.7 - '@firebase/component': 0.6.8 - '@firebase/logger': 0.4.2 - '@firebase/util': 1.9.7 - safevalues: 0.6.0 + '@firebase/app': 0.13.1 + '@firebase/component': 0.6.17 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 tslib: 2.7.0 - '@firebase/app-compat@0.2.37': + '@firebase/app-compat@0.4.1': dependencies: - '@firebase/app': 0.10.7 - '@firebase/component': 0.6.8 - '@firebase/logger': 0.4.2 - '@firebase/util': 1.9.7 + '@firebase/app': 0.13.1 + '@firebase/component': 0.6.17 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 tslib: 2.7.0 '@firebase/app-types@0.9.2': {} - '@firebase/app@0.10.7': + '@firebase/app-types@0.9.3': {} + + '@firebase/app@0.13.1': dependencies: - '@firebase/component': 0.6.8 - '@firebase/logger': 0.4.2 - '@firebase/util': 1.9.7 + '@firebase/component': 0.6.17 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 idb: 7.1.1 tslib: 2.7.0 - '@firebase/auth-compat@0.5.10(@firebase/app-compat@0.2.37)(@firebase/app-types@0.9.2)(@firebase/app@0.10.7)': + '@firebase/auth-compat@0.5.26(@firebase/app-compat@0.4.1)(@firebase/app-types@0.9.3)(@firebase/app@0.13.1)': dependencies: - '@firebase/app-compat': 0.2.37 - '@firebase/auth': 1.7.5(@firebase/app@0.10.7) - '@firebase/auth-types': 0.12.2(@firebase/app-types@0.9.2)(@firebase/util@1.9.7) - '@firebase/component': 0.6.8 - '@firebase/util': 1.9.7 + '@firebase/app-compat': 0.4.1 + '@firebase/auth': 1.10.6(@firebase/app@0.13.1) + '@firebase/auth-types': 0.13.0(@firebase/app-types@0.9.3)(@firebase/util@1.12.0) + '@firebase/component': 0.6.17 + '@firebase/util': 1.12.0 tslib: 2.7.0 - undici: 5.28.4 transitivePeerDependencies: - '@firebase/app' - '@firebase/app-types' - '@react-native-async-storage/async-storage' - '@firebase/auth-interop-types@0.2.3': {} + '@firebase/auth-interop-types@0.2.4': {} - '@firebase/auth-types@0.12.2(@firebase/app-types@0.9.2)(@firebase/util@1.9.7)': + '@firebase/auth-types@0.13.0(@firebase/app-types@0.9.3)(@firebase/util@1.12.0)': dependencies: - '@firebase/app-types': 0.9.2 - '@firebase/util': 1.9.7 + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.12.0 - '@firebase/auth@1.7.5(@firebase/app@0.10.7)': + '@firebase/auth@1.10.6(@firebase/app@0.13.1)': dependencies: - '@firebase/app': 0.10.7 - '@firebase/component': 0.6.8 - '@firebase/logger': 0.4.2 - '@firebase/util': 1.9.7 + '@firebase/app': 0.13.1 + '@firebase/component': 0.6.17 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 tslib: 2.7.0 - undici: 5.28.4 - '@firebase/component@0.6.8': + '@firebase/component@0.6.17': dependencies: - '@firebase/util': 1.9.7 + '@firebase/util': 1.12.0 tslib: 2.7.0 - '@firebase/database-compat@1.0.6': + '@firebase/data-connect@0.3.9(@firebase/app@0.13.1)': dependencies: - '@firebase/component': 0.6.8 - '@firebase/database': 1.0.6 - '@firebase/database-types': 1.0.4 - '@firebase/logger': 0.4.2 - '@firebase/util': 1.9.7 + '@firebase/app': 0.13.1 + '@firebase/auth-interop-types': 0.2.4 + '@firebase/component': 0.6.17 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 tslib: 2.7.0 - '@firebase/database-types@1.0.4': + '@firebase/database-compat@2.0.10': dependencies: - '@firebase/app-types': 0.9.2 - '@firebase/util': 1.9.7 + '@firebase/component': 0.6.17 + '@firebase/database': 1.0.19 + '@firebase/database-types': 1.0.14 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 + tslib: 2.7.0 + + '@firebase/database-types@1.0.14': + dependencies: + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.12.0 - '@firebase/database@1.0.6': + '@firebase/database@1.0.19': dependencies: - '@firebase/app-check-interop-types': 0.3.2 - '@firebase/auth-interop-types': 0.2.3 - '@firebase/component': 0.6.8 - '@firebase/logger': 0.4.2 - '@firebase/util': 1.9.7 + '@firebase/app-check-interop-types': 0.3.3 + '@firebase/auth-interop-types': 0.2.4 + '@firebase/component': 0.6.17 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 faye-websocket: 0.11.4 tslib: 2.7.0 - '@firebase/firestore-compat@0.3.33(@firebase/app-compat@0.2.37)(@firebase/app-types@0.9.2)(@firebase/app@0.10.7)': + '@firebase/firestore-compat@0.3.52(@firebase/app-compat@0.4.1)(@firebase/app-types@0.9.3)(@firebase/app@0.13.1)': dependencies: - '@firebase/app-compat': 0.2.37 - '@firebase/component': 0.6.8 - '@firebase/firestore': 4.6.4(@firebase/app@0.10.7) - '@firebase/firestore-types': 3.0.2(@firebase/app-types@0.9.2)(@firebase/util@1.9.7) - '@firebase/util': 1.9.7 + '@firebase/app-compat': 0.4.1 + '@firebase/component': 0.6.17 + '@firebase/firestore': 4.7.17(@firebase/app@0.13.1) + '@firebase/firestore-types': 3.0.3(@firebase/app-types@0.9.3)(@firebase/util@1.12.0) + '@firebase/util': 1.12.0 tslib: 2.7.0 transitivePeerDependencies: - '@firebase/app' - '@firebase/app-types' - '@firebase/firestore-types@3.0.2(@firebase/app-types@0.9.2)(@firebase/util@1.9.7)': + '@firebase/firestore-types@3.0.3(@firebase/app-types@0.9.3)(@firebase/util@1.12.0)': dependencies: - '@firebase/app-types': 0.9.2 - '@firebase/util': 1.9.7 + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.12.0 - '@firebase/firestore@4.6.4(@firebase/app@0.10.7)': + '@firebase/firestore@4.7.17(@firebase/app@0.13.1)': dependencies: - '@firebase/app': 0.10.7 - '@firebase/component': 0.6.8 - '@firebase/logger': 0.4.2 - '@firebase/util': 1.9.7 - '@firebase/webchannel-wrapper': 1.0.1 + '@firebase/app': 0.13.1 + '@firebase/component': 0.6.17 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 + '@firebase/webchannel-wrapper': 1.0.3 '@grpc/grpc-js': 1.9.15 '@grpc/proto-loader': 0.7.13 tslib: 2.7.0 - undici: 5.28.4 - '@firebase/functions-compat@0.3.12(@firebase/app-compat@0.2.37)(@firebase/app@0.10.7)': + '@firebase/functions-compat@0.3.25(@firebase/app-compat@0.4.1)(@firebase/app@0.13.1)': dependencies: - '@firebase/app-compat': 0.2.37 - '@firebase/component': 0.6.8 - '@firebase/functions': 0.11.6(@firebase/app@0.10.7) - '@firebase/functions-types': 0.6.2 - '@firebase/util': 1.9.7 + '@firebase/app-compat': 0.4.1 + '@firebase/component': 0.6.17 + '@firebase/functions': 0.12.8(@firebase/app@0.13.1) + '@firebase/functions-types': 0.6.3 + '@firebase/util': 1.12.0 tslib: 2.7.0 transitivePeerDependencies: - '@firebase/app' - '@firebase/functions-types@0.6.2': {} + '@firebase/functions-types@0.6.3': {} - '@firebase/functions@0.11.6(@firebase/app@0.10.7)': + '@firebase/functions@0.12.8(@firebase/app@0.13.1)': dependencies: - '@firebase/app': 0.10.7 - '@firebase/app-check-interop-types': 0.3.2 - '@firebase/auth-interop-types': 0.2.3 - '@firebase/component': 0.6.8 - '@firebase/messaging-interop-types': 0.2.2 - '@firebase/util': 1.9.7 + '@firebase/app': 0.13.1 + '@firebase/app-check-interop-types': 0.3.3 + '@firebase/auth-interop-types': 0.2.4 + '@firebase/component': 0.6.17 + '@firebase/messaging-interop-types': 0.2.3 + '@firebase/util': 1.12.0 tslib: 2.7.0 - undici: 5.28.4 - '@firebase/installations-compat@0.2.8(@firebase/app-compat@0.2.37)(@firebase/app-types@0.9.2)(@firebase/app@0.10.7)': + '@firebase/installations-compat@0.2.17(@firebase/app-compat@0.4.1)(@firebase/app-types@0.9.3)(@firebase/app@0.13.1)': dependencies: - '@firebase/app-compat': 0.2.37 - '@firebase/component': 0.6.8 - '@firebase/installations': 0.6.8(@firebase/app@0.10.7) - '@firebase/installations-types': 0.5.2(@firebase/app-types@0.9.2) - '@firebase/util': 1.9.7 + '@firebase/app-compat': 0.4.1 + '@firebase/component': 0.6.17 + '@firebase/installations': 0.6.17(@firebase/app@0.13.1) + '@firebase/installations-types': 0.5.3(@firebase/app-types@0.9.3) + '@firebase/util': 1.12.0 tslib: 2.7.0 transitivePeerDependencies: - '@firebase/app' - '@firebase/app-types' - '@firebase/installations-types@0.5.2(@firebase/app-types@0.9.2)': + '@firebase/installations-types@0.5.3(@firebase/app-types@0.9.3)': dependencies: - '@firebase/app-types': 0.9.2 + '@firebase/app-types': 0.9.3 - '@firebase/installations@0.6.8(@firebase/app@0.10.7)': + '@firebase/installations@0.6.17(@firebase/app@0.13.1)': dependencies: - '@firebase/app': 0.10.7 - '@firebase/component': 0.6.8 - '@firebase/util': 1.9.7 + '@firebase/app': 0.13.1 + '@firebase/component': 0.6.17 + '@firebase/util': 1.12.0 idb: 7.1.1 tslib: 2.7.0 - '@firebase/logger@0.4.2': + '@firebase/logger@0.4.4': dependencies: tslib: 2.7.0 - '@firebase/messaging-compat@0.2.10(@firebase/app-compat@0.2.37)(@firebase/app@0.10.7)': + '@firebase/messaging-compat@0.2.21(@firebase/app-compat@0.4.1)(@firebase/app@0.13.1)': dependencies: - '@firebase/app-compat': 0.2.37 - '@firebase/component': 0.6.8 - '@firebase/messaging': 0.12.10(@firebase/app@0.10.7) - '@firebase/util': 1.9.7 + '@firebase/app-compat': 0.4.1 + '@firebase/component': 0.6.17 + '@firebase/messaging': 0.12.21(@firebase/app@0.13.1) + '@firebase/util': 1.12.0 tslib: 2.7.0 transitivePeerDependencies: - '@firebase/app' - '@firebase/messaging-interop-types@0.2.2': {} + '@firebase/messaging-interop-types@0.2.3': {} - '@firebase/messaging@0.12.10(@firebase/app@0.10.7)': + '@firebase/messaging@0.12.21(@firebase/app@0.13.1)': dependencies: - '@firebase/app': 0.10.7 - '@firebase/component': 0.6.8 - '@firebase/installations': 0.6.8(@firebase/app@0.10.7) - '@firebase/messaging-interop-types': 0.2.2 - '@firebase/util': 1.9.7 + '@firebase/app': 0.13.1 + '@firebase/component': 0.6.17 + '@firebase/installations': 0.6.17(@firebase/app@0.13.1) + '@firebase/messaging-interop-types': 0.2.3 + '@firebase/util': 1.12.0 idb: 7.1.1 tslib: 2.7.0 - '@firebase/performance-compat@0.2.8(@firebase/app-compat@0.2.37)(@firebase/app@0.10.7)': + '@firebase/performance-compat@0.2.19(@firebase/app-compat@0.4.1)(@firebase/app@0.13.1)': dependencies: - '@firebase/app-compat': 0.2.37 - '@firebase/component': 0.6.8 - '@firebase/logger': 0.4.2 - '@firebase/performance': 0.6.8(@firebase/app@0.10.7) - '@firebase/performance-types': 0.2.2 - '@firebase/util': 1.9.7 + '@firebase/app-compat': 0.4.1 + '@firebase/component': 0.6.17 + '@firebase/logger': 0.4.4 + '@firebase/performance': 0.7.6(@firebase/app@0.13.1) + '@firebase/performance-types': 0.2.3 + '@firebase/util': 1.12.0 tslib: 2.7.0 transitivePeerDependencies: - '@firebase/app' - '@firebase/performance-types@0.2.2': {} + '@firebase/performance-types@0.2.3': {} - '@firebase/performance@0.6.8(@firebase/app@0.10.7)': + '@firebase/performance@0.7.6(@firebase/app@0.13.1)': dependencies: - '@firebase/app': 0.10.7 - '@firebase/component': 0.6.8 - '@firebase/installations': 0.6.8(@firebase/app@0.10.7) - '@firebase/logger': 0.4.2 - '@firebase/util': 1.9.7 + '@firebase/app': 0.13.1 + '@firebase/component': 0.6.17 + '@firebase/installations': 0.6.17(@firebase/app@0.13.1) + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 tslib: 2.7.0 + web-vitals: 4.2.4 - '@firebase/remote-config-compat@0.2.8(@firebase/app-compat@0.2.37)(@firebase/app@0.10.7)': + '@firebase/remote-config-compat@0.2.17(@firebase/app-compat@0.4.1)(@firebase/app@0.13.1)': dependencies: - '@firebase/app-compat': 0.2.37 - '@firebase/component': 0.6.8 - '@firebase/logger': 0.4.2 - '@firebase/remote-config': 0.4.8(@firebase/app@0.10.7) - '@firebase/remote-config-types': 0.3.2 - '@firebase/util': 1.9.7 + '@firebase/app-compat': 0.4.1 + '@firebase/component': 0.6.17 + '@firebase/logger': 0.4.4 + '@firebase/remote-config': 0.6.4(@firebase/app@0.13.1) + '@firebase/remote-config-types': 0.4.0 + '@firebase/util': 1.12.0 tslib: 2.7.0 transitivePeerDependencies: - '@firebase/app' - '@firebase/remote-config-types@0.3.2': {} + '@firebase/remote-config-types@0.4.0': {} - '@firebase/remote-config@0.4.8(@firebase/app@0.10.7)': + '@firebase/remote-config@0.6.4(@firebase/app@0.13.1)': dependencies: - '@firebase/app': 0.10.7 - '@firebase/component': 0.6.8 - '@firebase/installations': 0.6.8(@firebase/app@0.10.7) - '@firebase/logger': 0.4.2 - '@firebase/util': 1.9.7 + '@firebase/app': 0.13.1 + '@firebase/component': 0.6.17 + '@firebase/installations': 0.6.17(@firebase/app@0.13.1) + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 tslib: 2.7.0 - '@firebase/storage-compat@0.3.9(@firebase/app-compat@0.2.37)(@firebase/app-types@0.9.2)(@firebase/app@0.10.7)': + '@firebase/storage-compat@0.3.22(@firebase/app-compat@0.4.1)(@firebase/app-types@0.9.3)(@firebase/app@0.13.1)': dependencies: - '@firebase/app-compat': 0.2.37 - '@firebase/component': 0.6.8 - '@firebase/storage': 0.12.6(@firebase/app@0.10.7) - '@firebase/storage-types': 0.8.2(@firebase/app-types@0.9.2)(@firebase/util@1.9.7) - '@firebase/util': 1.9.7 + '@firebase/app-compat': 0.4.1 + '@firebase/component': 0.6.17 + '@firebase/storage': 0.13.12(@firebase/app@0.13.1) + '@firebase/storage-types': 0.8.3(@firebase/app-types@0.9.3)(@firebase/util@1.12.0) + '@firebase/util': 1.12.0 tslib: 2.7.0 transitivePeerDependencies: - '@firebase/app' - '@firebase/app-types' - '@firebase/storage-types@0.8.2(@firebase/app-types@0.9.2)(@firebase/util@1.9.7)': - dependencies: - '@firebase/app-types': 0.9.2 - '@firebase/util': 1.9.7 - - '@firebase/storage@0.12.6(@firebase/app@0.10.7)': + '@firebase/storage-types@0.8.3(@firebase/app-types@0.9.3)(@firebase/util@1.12.0)': dependencies: - '@firebase/app': 0.10.7 - '@firebase/component': 0.6.8 - '@firebase/util': 1.9.7 - tslib: 2.7.0 - undici: 5.28.4 + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.12.0 - '@firebase/util@1.9.7': + '@firebase/storage@0.13.12(@firebase/app@0.13.1)': dependencies: + '@firebase/app': 0.13.1 + '@firebase/component': 0.6.17 + '@firebase/util': 1.12.0 tslib: 2.7.0 - '@firebase/vertexai-preview@0.0.3(@firebase/app-types@0.9.2)(@firebase/app@0.10.7)': + '@firebase/util@1.12.0': dependencies: - '@firebase/app': 0.10.7 - '@firebase/app-check-interop-types': 0.3.2 - '@firebase/app-types': 0.9.2 - '@firebase/component': 0.6.8 - '@firebase/logger': 0.4.2 - '@firebase/util': 1.9.7 tslib: 2.7.0 - '@firebase/webchannel-wrapper@1.0.1': {} + '@firebase/webchannel-wrapper@1.0.3': {} '@floating-ui/core@1.6.0': dependencies: @@ -11360,8 +11473,9 @@ snapshots: '@fullcalendar/core': 6.1.11 vue: 3.5.8(typescript@5.4.5) - '@google-cloud/firestore@7.9.0(encoding@0.1.13)': + '@google-cloud/firestore@7.11.1(encoding@0.1.13)': dependencies: + '@opentelemetry/api': 1.9.0 fast-deep-equal: 3.1.3 functional-red-black-tree: 1.0.1 google-gax: 4.3.8(encoding@0.1.13) @@ -11383,7 +11497,7 @@ snapshots: '@google-cloud/promisify@4.0.0': optional: true - '@google-cloud/storage@7.12.0(encoding@0.1.13)': + '@google-cloud/storage@7.16.0(encoding@0.1.13)': dependencies: '@google-cloud/paginator': 5.0.2 '@google-cloud/projectify': 4.0.0 @@ -11393,7 +11507,7 @@ snapshots: duplexify: 4.1.3 fast-xml-parser: 4.4.1 gaxios: 6.7.0(encoding@0.1.13) - google-auth-library: 9.11.0(encoding@0.1.13) + google-auth-library: 9.15.1(encoding@0.1.13) html-entities: 2.5.2 mime: 3.0.0 p-limit: 3.1.0 @@ -11423,8 +11537,6 @@ snapshots: protobufjs: 7.3.2 yargs: 17.7.2 - '@hotjar/browser@1.0.9': {} - '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -11595,7 +11707,7 @@ snapshots: '@jsdevtools/ez-spawn@3.0.4': dependencies: call-me-maybe: 1.0.2 - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 string-argv: 0.3.2 type-detect: 4.0.8 @@ -11681,6 +11793,10 @@ snapshots: json5: 2.2.3 rollup: 3.29.5 + '@mongodb-js/saslprep@1.2.2': + dependencies: + sparse-bitfield: 3.0.3 + '@netlify/functions@2.8.1': dependencies: '@netlify/serverless-functions-api': 1.19.1 @@ -11855,7 +11971,7 @@ snapshots: - vite - webpack-sources - '@nuxt/fonts@0.9.2(encoding@0.1.13)(ioredis@5.4.1)(magicast@0.3.5)(rollup@3.29.5)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(webpack-sources@3.2.3)': + '@nuxt/fonts@0.9.2(@upstash/redis@1.35.0)(db0@0.1.4(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4)))(encoding@0.1.13)(ioredis@5.6.1)(magicast@0.3.5)(rollup@3.29.5)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(webpack-sources@3.2.3)': dependencies: '@nuxt/devtools-kit': 1.5.1(magicast@0.3.5)(rollup@3.29.5)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(webpack-sources@3.2.3) '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@3.29.5)(webpack-sources@3.2.3) @@ -11875,7 +11991,7 @@ snapshots: tinyglobby: 0.2.9 ufo: 1.5.4 unplugin: 1.14.1(webpack-sources@3.2.3) - unstorage: 1.12.0(ioredis@5.4.1) + unstorage: 1.16.0(@upstash/redis@1.35.0)(db0@0.1.4(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4)))(ioredis@5.6.1) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -11884,10 +12000,14 @@ snapshots: - '@azure/keyvault-secrets' - '@azure/storage-blob' - '@capacitor/preferences' + - '@deno/kv' - '@netlify/blobs' - '@planetscale/database' - '@upstash/redis' + - '@vercel/blob' - '@vercel/kv' + - aws4fetch + - db0 - encoding - idb-keyval - ioredis @@ -11895,6 +12015,7 @@ snapshots: - rollup - supports-color - uWebSockets.js + - uploadthing - vite - webpack-sources @@ -11936,7 +12057,7 @@ snapshots: globby: 14.0.2 hash-sum: 2.0.0 ignore: 6.0.2 - jiti: 2.4.0 + jiti: 2.4.2 klona: 2.0.6 knitwork: 1.1.0 mlly: 1.7.3 @@ -12032,7 +12153,7 @@ snapshots: is-docker: 3.0.0 jiti: 1.21.6 mri: 1.2.0 - nanoid: 5.0.7 + nanoid: 5.1.5 ofetch: 1.3.4 package-manager-detector: 0.2.0 parse-git-config: 3.0.0 @@ -12485,6 +12606,8 @@ snapshots: transitivePeerDependencies: - debug + '@petamoriken/float16@3.9.2': {} + '@pinia/nuxt@0.5.1(magicast@0.3.5)(rollup@3.29.5)(typescript@5.4.5)(vue@3.5.8(typescript@5.4.5))(webpack-sources@3.2.3)': dependencies: '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@3.29.5)(webpack-sources@3.2.3) @@ -12543,19 +12666,6 @@ snapshots: '@protobufjs/utf8@1.1.0': {} - '@puppeteer/browsers@2.4.0': - dependencies: - debug: 4.3.7 - extract-zip: 2.0.1 - progress: 2.0.3 - proxy-agent: 6.4.0 - semver: 7.6.3 - tar-fs: 3.0.6 - unbzip2-stream: 1.4.3 - yargs: 17.7.2 - transitivePeerDependencies: - - supports-color - '@remirror/core-constants@2.0.2': {} '@rollup/plugin-alias@5.1.0(rollup@3.29.5)': @@ -12880,7 +12990,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@sentry/nuxt@8.37.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.27.0)(encoding@0.1.13)(magicast@0.3.5)(nuxt@3.13.2(@parcel/watcher@2.4.1)(@types/node@20.12.7)(drizzle-orm@0.32.2(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(pg@8.12.0)(postgres@3.4.4)(react@18.2.0))(encoding@0.1.13)(eslint@8.57.0)(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.3)(rollup@3.29.5)(sass@1.75.0)(stylelint@16.2.1(typescript@5.4.5))(terser@5.30.4)(typescript@5.4.5)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(webpack-sources@3.2.3))(pinia@2.1.7(typescript@5.4.5)(vue@3.5.8(typescript@5.4.5)))(rollup@3.29.5)(vue@3.5.8(typescript@5.4.5))(webpack-sources@3.2.3)': + '@sentry/nuxt@8.37.0(bd6072b168615c8ea944b69150988573)': dependencies: '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@3.29.5)(webpack-sources@3.2.3) '@sentry/browser': 8.37.0 @@ -12892,7 +13002,7 @@ snapshots: '@sentry/utils': 8.37.0 '@sentry/vite-plugin': 2.22.6(encoding@0.1.13) '@sentry/vue': 8.37.0(pinia@2.1.7(typescript@5.4.5)(vue@3.5.8(typescript@5.4.5)))(vue@3.5.8(typescript@5.4.5)) - nuxt: 3.13.2(@parcel/watcher@2.4.1)(@types/node@20.12.7)(drizzle-orm@0.32.2(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(pg@8.12.0)(postgres@3.4.4)(react@18.2.0))(encoding@0.1.13)(eslint@8.57.0)(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.3)(rollup@3.29.5)(sass@1.75.0)(stylelint@16.2.1(typescript@5.4.5))(terser@5.30.4)(typescript@5.4.5)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(webpack-sources@3.2.3) + nuxt: 3.13.2(@parcel/watcher@2.4.1)(@types/node@20.12.7)(@upstash/redis@1.35.0)(db0@0.1.4(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4)))(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4))(encoding@0.1.13)(eslint@8.57.0)(ioredis@5.6.1)(magicast@0.3.5)(optionator@0.9.3)(rollup@3.29.5)(sass@1.75.0)(stylelint@16.2.1(typescript@5.4.5))(terser@5.30.4)(typescript@5.4.5)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(webpack-sources@3.2.3) transitivePeerDependencies: - '@opentelemetry/api' - '@opentelemetry/core' @@ -13323,22 +13433,6 @@ snapshots: - supports-color - typescript - '@stylistic/stylelint-config@1.0.1(stylelint@16.2.1(typescript@5.4.5))': - dependencies: - '@stylistic/stylelint-plugin': 2.1.1(stylelint@16.2.1(typescript@5.4.5)) - stylelint: 16.2.1(typescript@5.4.5) - - '@stylistic/stylelint-plugin@2.1.1(stylelint@16.2.1(typescript@5.4.5))': - dependencies: - '@csstools/css-parser-algorithms': 2.6.1(@csstools/css-tokenizer@2.2.4) - '@csstools/css-tokenizer': 2.2.4 - '@csstools/media-query-list-parser': 2.1.9(@csstools/css-parser-algorithms@2.6.1(@csstools/css-tokenizer@2.2.4))(@csstools/css-tokenizer@2.2.4) - is-plain-object: 5.0.0 - postcss-selector-parser: 6.1.2 - postcss-value-parser: 4.2.0 - style-search: 0.1.0 - stylelint: 16.2.1(typescript@5.4.5) - '@swc/helpers@0.5.13': dependencies: tslib: 2.7.0 @@ -13532,8 +13626,6 @@ snapshots: '@tootallnate/once@2.0.0': optional: true - '@tootallnate/quickjs-emscripten@0.23.0': {} - '@trysound/sax@0.2.0': {} '@types/bcrypt@5.0.2': @@ -13559,6 +13651,7 @@ snapshots: '@types/cors@2.8.17': dependencies: '@types/node': 20.12.7 + optional: true '@types/eslint@8.56.10': dependencies: @@ -13586,6 +13679,7 @@ snapshots: '@types/body-parser': 1.19.5 '@types/express-serve-static-core': 4.19.5 '@types/serve-static': 1.15.7 + optional: true '@types/geojson@7946.0.14': {} @@ -13628,6 +13722,10 @@ snapshots: dependencies: undici-types: 5.26.5 + '@types/node@22.15.30': + dependencies: + undici-types: 6.21.0 + '@types/nodemailer@6.4.16': dependencies: '@types/node': 20.12.7 @@ -13696,6 +13794,8 @@ snapshots: '@types/tough-cookie@4.0.5': optional: true + '@types/triple-beam@1.3.5': {} + '@types/unist@2.0.10': {} '@types/uuid@8.3.4': {} @@ -13706,6 +13806,12 @@ snapshots: '@types/webfontloader@1.6.38': {} + '@types/webidl-conversions@7.0.3': {} + + '@types/whatwg-url@11.0.5': + dependencies: + '@types/webidl-conversions': 7.0.3 + '@types/yauzl@2.10.3': dependencies: '@types/node': 20.12.7 @@ -14132,9 +14238,13 @@ snapshots: unhead: 1.11.6 vue: 3.5.8(typescript@5.4.5) - '@vercel/nft@0.26.5(encoding@0.1.13)': + '@upstash/redis@1.35.0': dependencies: - '@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13) + uncrypto: 0.1.3 + + '@vercel/nft@0.26.5(encoding@0.1.13)': + dependencies: + '@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13) '@rollup/pluginutils': 4.2.1 acorn: 8.14.0 acorn-import-attributes: 1.9.5(acorn@8.14.0) @@ -14382,13 +14492,13 @@ snapshots: '@vue/shared@3.5.8': {} - '@vuetify/loader-shared@2.0.3(vue@3.5.8(typescript@5.4.5))(vuetify@3.5.15(typescript@5.4.5)(vite-plugin-vuetify@2.0.3)(vue-i18n@9.13.1(vue@3.5.8(typescript@5.4.5)))(vue@3.5.8(typescript@5.4.5)))': + '@vuetify/loader-shared@2.0.3(vue@3.5.8(typescript@5.4.5))(vuetify@3.5.15)': dependencies: upath: 2.0.1 vue: 3.5.8(typescript@5.4.5) vuetify: 3.5.15(typescript@5.4.5)(vite-plugin-vuetify@2.0.3)(vue-i18n@9.13.1(vue@3.5.8(typescript@5.4.5)))(vue@3.5.8(typescript@5.4.5)) - '@vuetify/loader-shared@2.0.3(vue@3.5.8(typescript@5.4.5))(vuetify@3.7.2(typescript@5.4.5)(vite-plugin-vuetify@2.0.4)(vue@3.5.8(typescript@5.4.5)))': + '@vuetify/loader-shared@2.0.3(vue@3.5.8(typescript@5.4.5))(vuetify@3.7.2)': dependencies: upath: 2.0.1 vue: 3.5.8(typescript@5.4.5) @@ -14426,13 +14536,13 @@ snapshots: '@vueuse/metadata@10.9.0': {} - '@vueuse/nuxt@10.11.0(magicast@0.3.5)(nuxt@3.13.2(@parcel/watcher@2.4.1)(@types/node@20.12.7)(drizzle-orm@0.32.2(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(pg@8.12.0)(postgres@3.4.4)(react@18.2.0))(encoding@0.1.13)(eslint@8.57.0)(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.3)(rollup@3.29.5)(sass@1.75.0)(stylelint@16.2.1(typescript@5.4.5))(terser@5.30.4)(typescript@5.4.5)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(webpack-sources@3.2.3))(rollup@3.29.5)(vue@3.5.8(typescript@5.4.5))(webpack-sources@3.2.3)': + '@vueuse/nuxt@10.11.0(magicast@0.3.5)(nuxt@3.13.2(@parcel/watcher@2.4.1)(@types/node@20.12.7)(@upstash/redis@1.35.0)(db0@0.1.4(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4)))(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4))(encoding@0.1.13)(eslint@8.57.0)(ioredis@5.6.1)(magicast@0.3.5)(optionator@0.9.3)(rollup@3.29.5)(sass@1.75.0)(stylelint@16.2.1(typescript@5.4.5))(terser@5.30.4)(typescript@5.4.5)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(webpack-sources@3.2.3))(rollup@3.29.5)(vue@3.5.8(typescript@5.4.5))(webpack-sources@3.2.3)': dependencies: '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@3.29.5)(webpack-sources@3.2.3) '@vueuse/core': 10.11.0(vue@3.5.8(typescript@5.4.5)) '@vueuse/metadata': 10.11.0 local-pkg: 0.5.0 - nuxt: 3.13.2(@parcel/watcher@2.4.1)(@types/node@20.12.7)(drizzle-orm@0.32.2(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(pg@8.12.0)(postgres@3.4.4)(react@18.2.0))(encoding@0.1.13)(eslint@8.57.0)(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.3)(rollup@3.29.5)(sass@1.75.0)(stylelint@16.2.1(typescript@5.4.5))(terser@5.30.4)(typescript@5.4.5)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(webpack-sources@3.2.3) + nuxt: 3.13.2(@parcel/watcher@2.4.1)(@types/node@20.12.7)(@upstash/redis@1.35.0)(db0@0.1.4(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4)))(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4))(encoding@0.1.13)(eslint@8.57.0)(ioredis@5.6.1)(magicast@0.3.5)(optionator@0.9.3)(rollup@3.29.5)(sass@1.75.0)(stylelint@16.2.1(typescript@5.4.5))(terser@5.30.4)(typescript@5.4.5)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(webpack-sources@3.2.3) vue-demi: 0.14.10(vue@3.5.8(typescript@5.4.5)) transitivePeerDependencies: - '@vue/composition-api' @@ -14470,6 +14580,7 @@ snapshots: dependencies: mime-types: 2.1.35 negotiator: 0.6.3 + optional: true acorn-import-attributes@1.9.5(acorn@8.14.0): dependencies: @@ -14584,7 +14695,8 @@ snapshots: argparse@2.0.1: {} - array-flatten@1.1.1: {} + array-flatten@1.1.1: + optional: true array-union@2.1.0: {} @@ -14603,10 +14715,6 @@ snapshots: '@babel/parser': 7.25.6 pathe: 1.1.2 - ast-types@0.13.4: - dependencies: - tslib: 2.7.0 - ast-walker-scope@0.6.2: dependencies: '@babel/parser': 7.25.6 @@ -14652,27 +14760,6 @@ snapshots: bare-events@2.2.2: optional: true - bare-fs@2.3.5: - dependencies: - bare-events: 2.2.2 - bare-path: 2.1.3 - bare-stream: 2.3.0 - optional: true - - bare-os@2.4.4: - optional: true - - bare-path@2.1.3: - dependencies: - bare-os: 2.4.4 - optional: true - - bare-stream@2.3.0: - dependencies: - b4a: 1.6.6 - streamx: 2.20.1 - optional: true - base64-js@1.3.1: {} base64-js@1.5.1: {} @@ -14681,8 +14768,6 @@ snapshots: dependencies: safe-buffer: 5.1.2 - basic-ftp@5.0.5: {} - bcrypt@5.1.1(encoding@0.1.13): dependencies: '@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13) @@ -14691,8 +14776,7 @@ snapshots: - encoding - supports-color - bignumber.js@9.1.2: - optional: true + bignumber.js@9.1.2: {} binary-extensions@2.3.0: {} @@ -14720,6 +14804,7 @@ snapshots: unpipe: 1.0.0 transitivePeerDependencies: - supports-color + optional: true boolbase@1.0.0: {} @@ -14756,6 +14841,8 @@ snapshots: node-releases: 2.0.18 update-browserslist-db: 1.1.1(browserslist@4.24.2) + bson@6.10.4: {} + buffer-crc32@0.2.13: {} buffer-crc32@1.0.0: {} @@ -14764,11 +14851,6 @@ snapshots: buffer-from@1.1.2: {} - buffer@5.7.1: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - buffer@6.0.3: dependencies: base64-js: 1.5.1 @@ -14794,7 +14876,8 @@ snapshots: esbuild: 0.23.1 load-tsconfig: 0.2.5 - bytes@3.1.2: {} + bytes@3.1.2: + optional: true c12@1.11.2(magicast@0.3.5): dependencies: @@ -14820,7 +14903,7 @@ snapshots: defu: 6.1.4 dotenv: 16.4.5 giget: 1.2.3 - jiti: 2.4.0 + jiti: 2.4.2 mlly: 1.7.3 ohash: 1.1.4 pathe: 1.1.2 @@ -14936,14 +15019,11 @@ snapshots: dependencies: readdirp: 4.0.2 - chownr@2.0.0: {} - - chromium-bidi@0.6.5(devtools-protocol@0.0.1342118): + chokidar@4.0.3: dependencies: - devtools-protocol: 0.0.1342118 - mitt: 3.0.1 - urlpattern-polyfill: 10.0.0 - zod: 3.23.8 + readdirp: 4.0.2 + + chownr@2.0.0: {} ci-info@3.9.0: {} @@ -14991,10 +15071,25 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + color-support@1.1.3: {} + color@3.2.1: + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + colord@2.9.3: {} + colorspace@1.1.4: + dependencies: + color: 3.2.1 + text-hex: 1.0.0 + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -15040,8 +15135,10 @@ snapshots: content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 + optional: true - content-type@1.0.5: {} + content-type@1.0.5: + optional: true convert-source-map@2.0.0: {} @@ -15049,9 +15146,11 @@ snapshots: cookie-es@1.2.2: {} - cookie-signature@1.0.6: {} + cookie-signature@1.0.6: + optional: true - cookie@0.6.0: {} + cookie@0.6.0: + optional: true copy-anything@3.0.5: dependencies: @@ -15067,6 +15166,7 @@ snapshots: dependencies: object-assign: 4.1.1 vary: 1.1.2 + optional: true cosmiconfig@9.0.0(typescript@5.4.5): dependencies: @@ -15104,6 +15204,12 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + crossws@0.2.4: {} crypto-js@4.2.0: {} @@ -15197,13 +15303,11 @@ snapshots: csstype@3.1.3: {} - data-uri-to-buffer@6.0.2: {} - date-fns@4.1.0: {} - db0@0.1.4(drizzle-orm@0.32.2(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(pg@8.12.0)(postgres@3.4.4)(react@18.2.0)): + db0@0.1.4(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4)): optionalDependencies: - drizzle-orm: 0.32.2(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(pg@8.12.0)(postgres@3.4.4)(react@18.2.0) + drizzle-orm: 0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4) debug@2.6.9: dependencies: @@ -15257,12 +15361,6 @@ snapshots: defu@6.1.4: {} - degenerator@5.0.1: - dependencies: - ast-types: 0.13.4 - escodegen: 2.1.0 - esprima: 4.0.1 - delayed-stream@1.0.0: {} delegates@1.0.0: {} @@ -15273,6 +15371,8 @@ snapshots: destr@2.0.3: {} + destr@2.0.5: {} + destroy@1.2.0: {} detect-libc@1.0.3: {} @@ -15281,8 +15381,6 @@ snapshots: devalue@5.0.0: {} - devtools-protocol@0.0.1342118: {} - dfa@1.2.0: {} diff@7.0.0: {} @@ -15303,7 +15401,7 @@ snapshots: dependencies: '@types/node': 20.12.7 jszip: 3.10.1 - nanoid: 5.0.7 + nanoid: 5.1.5 xml: 1.0.1 xml-js: 1.6.11 @@ -15338,27 +15436,28 @@ snapshots: dotenv@16.4.5: {} - drizzle-kit@0.23.2: + drizzle-kit@0.30.6: dependencies: - '@drizzle-team/brocli': 0.8.2 + '@drizzle-team/brocli': 0.10.2 '@esbuild-kit/esm-loader': 2.6.5 esbuild: 0.19.12 esbuild-register: 3.6.0(esbuild@0.19.12) + gel: 2.1.0 transitivePeerDependencies: - supports-color - drizzle-orm@0.32.2(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(pg@8.12.0)(postgres@3.4.4)(react@18.2.0): + drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4): optionalDependencies: '@opentelemetry/api': 1.9.0 '@prisma/client': 5.19.1 '@types/pg': 8.11.8 + gel: 2.1.0 pg: 8.12.0 postgres: 3.4.4 - react: 18.2.0 - drizzle-zod@0.5.1(drizzle-orm@0.32.2(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(pg@8.12.0)(postgres@3.4.4)(react@18.2.0))(zod@3.23.8): + drizzle-zod@0.7.1(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4))(zod@3.23.8): dependencies: - drizzle-orm: 0.32.2(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(pg@8.12.0)(postgres@3.4.4)(react@18.2.0) + drizzle-orm: 0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4) zod: 3.23.8 duplexer@0.1.2: {} @@ -15389,6 +15488,8 @@ snapshots: emoji-regex@9.2.2: {} + enabled@2.0.0: {} + encodeurl@1.0.2: {} encoding@0.1.13: @@ -15409,6 +15510,8 @@ snapshots: env-paths@2.2.1: {} + env-paths@3.0.0: {} + error-ex@1.3.2: dependencies: is-arrayish: 0.2.1 @@ -15967,13 +16070,13 @@ snapshots: optionalDependencies: '@typescript-eslint/eslint-plugin': 8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) - eslint-plugin-vitest@0.5.4(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)(vitest@2.1.1(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4)): + eslint-plugin-vitest@0.5.4(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)(vitest@2.1.1(@types/node@22.15.30)(sass@1.75.0)(terser@5.30.4)): dependencies: '@typescript-eslint/utils': 7.7.1(eslint@8.57.0)(typescript@5.4.5) eslint: 8.57.0 optionalDependencies: '@typescript-eslint/eslint-plugin': 8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) - vitest: 2.1.1(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4) + vitest: 2.1.1(@types/node@22.15.30)(sass@1.75.0)(terser@5.30.4) transitivePeerDependencies: - supports-color - typescript @@ -16141,7 +16244,7 @@ snapshots: execa@7.2.0: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 get-stream: 6.0.1 human-signals: 4.3.1 is-stream: 3.0.0 @@ -16153,7 +16256,7 @@ snapshots: execa@8.0.1: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 get-stream: 8.0.1 human-signals: 5.0.0 is-stream: 3.0.0 @@ -16163,17 +16266,17 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 - execa@9.3.0: + execa@9.6.0: dependencies: '@sindresorhus/merge-streams': 4.0.0 - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 figures: 6.1.0 get-stream: 9.0.1 - human-signals: 7.0.0 + human-signals: 8.0.1 is-plain-obj: 4.1.0 is-stream: 4.0.1 - npm-run-path: 5.3.0 - pretty-ms: 9.1.0 + npm-run-path: 6.0.0 + pretty-ms: 9.2.0 signal-exit: 4.1.0 strip-final-newline: 4.0.0 yoctocolors: 2.1.1 @@ -16213,11 +16316,11 @@ snapshots: vary: 1.1.2 transitivePeerDependencies: - supports-color + optional: true exsolve@1.0.1: {} - extend@3.0.2: - optional: true + extend@3.0.2: {} externality@1.0.2: dependencies: @@ -16286,6 +16389,8 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fecha@4.2.3: {} + figures@6.1.0: dependencies: is-unicode-supported: 2.0.0 @@ -16298,6 +16403,10 @@ snapshots: dependencies: flat-cache: 4.0.1 + file-stream-rotator@0.6.1: + dependencies: + moment: 2.30.1 + file-uri-to-path@1.0.0: {} fill-range@7.0.1: @@ -16315,6 +16424,7 @@ snapshots: unpipe: 1.0.0 transitivePeerDependencies: - supports-color + optional: true find-up-simple@1.0.0: {} @@ -16334,65 +16444,67 @@ snapshots: path-exists: 5.0.0 unicorn-magic: 0.1.0 - firebase-admin@12.2.0(encoding@0.1.13): + firebase-admin@13.4.0(encoding@0.1.13): dependencies: - '@fastify/busboy': 2.1.1 - '@firebase/database-compat': 1.0.6 - '@firebase/database-types': 1.0.4 - '@types/node': 20.12.7 + '@fastify/busboy': 3.1.1 + '@firebase/database-compat': 2.0.10 + '@firebase/database-types': 1.0.14 + '@types/node': 22.15.30 farmhash-modern: 1.1.0 + google-auth-library: 9.15.1(encoding@0.1.13) jsonwebtoken: 9.0.2 jwks-rsa: 3.1.0 - long: 5.2.3 node-forge: 1.3.1 - uuid: 10.0.0 + uuid: 11.1.0 optionalDependencies: - '@google-cloud/firestore': 7.9.0(encoding@0.1.13) - '@google-cloud/storage': 7.12.0(encoding@0.1.13) + '@google-cloud/firestore': 7.11.1(encoding@0.1.13) + '@google-cloud/storage': 7.16.0(encoding@0.1.13) transitivePeerDependencies: - encoding - supports-color - firebase-functions@5.0.1(firebase-admin@12.2.0(encoding@0.1.13)): + firebase-functions@5.0.1(firebase-admin@13.4.0(encoding@0.1.13)): dependencies: '@types/cors': 2.8.17 '@types/express': 4.17.3 cors: 2.8.5 express: 4.19.2 - firebase-admin: 12.2.0(encoding@0.1.13) + firebase-admin: 13.4.0(encoding@0.1.13) protobufjs: 7.3.2 transitivePeerDependencies: - supports-color + optional: true - firebase@10.12.4: - dependencies: - '@firebase/analytics': 0.10.6(@firebase/app@0.10.7) - '@firebase/analytics-compat': 0.2.12(@firebase/app-compat@0.2.37)(@firebase/app@0.10.7) - '@firebase/app': 0.10.7 - '@firebase/app-check': 0.8.6(@firebase/app@0.10.7) - '@firebase/app-check-compat': 0.3.13(@firebase/app-compat@0.2.37)(@firebase/app@0.10.7) - '@firebase/app-compat': 0.2.37 - '@firebase/app-types': 0.9.2 - '@firebase/auth': 1.7.5(@firebase/app@0.10.7) - '@firebase/auth-compat': 0.5.10(@firebase/app-compat@0.2.37)(@firebase/app-types@0.9.2)(@firebase/app@0.10.7) - '@firebase/database': 1.0.6 - '@firebase/database-compat': 1.0.6 - '@firebase/firestore': 4.6.4(@firebase/app@0.10.7) - '@firebase/firestore-compat': 0.3.33(@firebase/app-compat@0.2.37)(@firebase/app-types@0.9.2)(@firebase/app@0.10.7) - '@firebase/functions': 0.11.6(@firebase/app@0.10.7) - '@firebase/functions-compat': 0.3.12(@firebase/app-compat@0.2.37)(@firebase/app@0.10.7) - '@firebase/installations': 0.6.8(@firebase/app@0.10.7) - '@firebase/installations-compat': 0.2.8(@firebase/app-compat@0.2.37)(@firebase/app-types@0.9.2)(@firebase/app@0.10.7) - '@firebase/messaging': 0.12.10(@firebase/app@0.10.7) - '@firebase/messaging-compat': 0.2.10(@firebase/app-compat@0.2.37)(@firebase/app@0.10.7) - '@firebase/performance': 0.6.8(@firebase/app@0.10.7) - '@firebase/performance-compat': 0.2.8(@firebase/app-compat@0.2.37)(@firebase/app@0.10.7) - '@firebase/remote-config': 0.4.8(@firebase/app@0.10.7) - '@firebase/remote-config-compat': 0.2.8(@firebase/app-compat@0.2.37)(@firebase/app@0.10.7) - '@firebase/storage': 0.12.6(@firebase/app@0.10.7) - '@firebase/storage-compat': 0.3.9(@firebase/app-compat@0.2.37)(@firebase/app-types@0.9.2)(@firebase/app@0.10.7) - '@firebase/util': 1.9.7 - '@firebase/vertexai-preview': 0.0.3(@firebase/app-types@0.9.2)(@firebase/app@0.10.7) + firebase@11.9.0: + dependencies: + '@firebase/ai': 1.4.0(@firebase/app-types@0.9.3)(@firebase/app@0.13.1) + '@firebase/analytics': 0.10.16(@firebase/app@0.13.1) + '@firebase/analytics-compat': 0.2.22(@firebase/app-compat@0.4.1)(@firebase/app@0.13.1) + '@firebase/app': 0.13.1 + '@firebase/app-check': 0.10.0(@firebase/app@0.13.1) + '@firebase/app-check-compat': 0.3.25(@firebase/app-compat@0.4.1)(@firebase/app@0.13.1) + '@firebase/app-compat': 0.4.1 + '@firebase/app-types': 0.9.3 + '@firebase/auth': 1.10.6(@firebase/app@0.13.1) + '@firebase/auth-compat': 0.5.26(@firebase/app-compat@0.4.1)(@firebase/app-types@0.9.3)(@firebase/app@0.13.1) + '@firebase/data-connect': 0.3.9(@firebase/app@0.13.1) + '@firebase/database': 1.0.19 + '@firebase/database-compat': 2.0.10 + '@firebase/firestore': 4.7.17(@firebase/app@0.13.1) + '@firebase/firestore-compat': 0.3.52(@firebase/app-compat@0.4.1)(@firebase/app-types@0.9.3)(@firebase/app@0.13.1) + '@firebase/functions': 0.12.8(@firebase/app@0.13.1) + '@firebase/functions-compat': 0.3.25(@firebase/app-compat@0.4.1)(@firebase/app@0.13.1) + '@firebase/installations': 0.6.17(@firebase/app@0.13.1) + '@firebase/installations-compat': 0.2.17(@firebase/app-compat@0.4.1)(@firebase/app-types@0.9.3)(@firebase/app@0.13.1) + '@firebase/messaging': 0.12.21(@firebase/app@0.13.1) + '@firebase/messaging-compat': 0.2.21(@firebase/app-compat@0.4.1)(@firebase/app@0.13.1) + '@firebase/performance': 0.7.6(@firebase/app@0.13.1) + '@firebase/performance-compat': 0.2.19(@firebase/app-compat@0.4.1)(@firebase/app@0.13.1) + '@firebase/remote-config': 0.6.4(@firebase/app@0.13.1) + '@firebase/remote-config-compat': 0.2.17(@firebase/app-compat@0.4.1)(@firebase/app@0.13.1) + '@firebase/storage': 0.13.12(@firebase/app@0.13.1) + '@firebase/storage-compat': 0.3.22(@firebase/app-compat@0.4.1)(@firebase/app-types@0.9.3)(@firebase/app@0.13.1) + '@firebase/util': 1.12.0 transitivePeerDependencies: - '@react-native-async-storage/async-storage' @@ -16411,6 +16523,8 @@ snapshots: flatted@3.3.1: {} + fn.name@1.1.0: {} + follow-redirects@1.15.6: {} fontaine@0.5.0(encoding@0.1.13)(webpack-sources@3.2.3): @@ -16440,7 +16554,7 @@ snapshots: foreground-child@3.1.1: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 signal-exit: 4.1.0 form-data@2.5.1: @@ -16456,7 +16570,8 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 - forwarded@0.2.0: {} + forwarded@0.2.0: + optional: true fraction.js@4.3.7: {} @@ -16506,7 +16621,6 @@ snapshots: transitivePeerDependencies: - encoding - supports-color - optional: true gcp-metadata@6.1.0(encoding@0.1.13): dependencies: @@ -16515,7 +16629,17 @@ snapshots: transitivePeerDependencies: - encoding - supports-color - optional: true + + gel@2.1.0: + dependencies: + '@petamoriken/float16': 3.9.2 + debug: 4.3.7 + env-paths: 3.0.0 + semver: 7.6.3 + shell-quote: 1.8.1 + which: 4.0.0 + transitivePeerDependencies: + - supports-color gensync@1.0.0-beta.2: {} @@ -16552,15 +16676,6 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - get-uri@6.0.3: - dependencies: - basic-ftp: 5.0.5 - data-uri-to-buffer: 6.0.2 - debug: 4.3.7 - fs-extra: 11.2.0 - transitivePeerDependencies: - - supports-color - giget@1.2.3: dependencies: citty: 0.1.6 @@ -16691,7 +16806,7 @@ snapshots: globjoin@0.1.4: {} - google-auth-library@9.11.0(encoding@0.1.13): + google-auth-library@9.15.1(encoding@0.1.13): dependencies: base64-js: 1.5.1 ecdsa-sig-formatter: 1.0.11 @@ -16702,7 +16817,6 @@ snapshots: transitivePeerDependencies: - encoding - supports-color - optional: true google-gax@4.3.8(encoding@0.1.13): dependencies: @@ -16711,7 +16825,7 @@ snapshots: '@types/long': 4.0.2 abort-controller: 3.0.0 duplexify: 4.1.3 - google-auth-library: 9.11.0(encoding@0.1.13) + google-auth-library: 9.15.1(encoding@0.1.13) node-fetch: 2.7.0(encoding@0.1.13) object-hash: 3.0.0 proto3-json-serializer: 2.0.2 @@ -16740,7 +16854,6 @@ snapshots: transitivePeerDependencies: - encoding - supports-color - optional: true gzip-size@7.0.0: dependencies: @@ -16820,13 +16933,6 @@ snapshots: - supports-color optional: true - http-proxy-agent@7.0.2: - dependencies: - agent-base: 7.1.1 - debug: 4.3.7 - transitivePeerDependencies: - - supports-color - http-shutdown@1.2.2: {} https-proxy-agent@5.0.1: @@ -16851,11 +16957,12 @@ snapshots: human-signals@5.0.0: {} - human-signals@7.0.0: {} + human-signals@8.0.1: {} iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 + optional: true iconv-lite@0.6.3: dependencies: @@ -16929,7 +17036,7 @@ snapshots: ini@4.1.1: {} - ioredis@5.4.1: + ioredis@5.6.1: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 @@ -16943,12 +17050,8 @@ snapshots: transitivePeerDependencies: - supports-color - ip-address@9.0.5: - dependencies: - jsbn: 1.1.0 - sprintf-js: 1.1.3 - - ipaddr.js@1.9.1: {} + ipaddr.js@1.9.1: + optional: true iron-webcrypto@1.2.1: {} @@ -16966,6 +17069,8 @@ snapshots: is-arrayish@0.2.1: {} + is-arrayish@0.3.2: {} + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -17064,6 +17169,8 @@ snapshots: isexe@2.0.0: {} + isexe@3.1.1: {} + jackspeak@2.3.6: dependencies: '@isaacs/cliui': 8.0.2 @@ -17080,8 +17187,6 @@ snapshots: jiti@2.0.0-beta.3: {} - jiti@2.4.0: {} - jiti@2.4.2: {} jose@4.15.5: {} @@ -17102,8 +17207,6 @@ snapshots: dependencies: argparse: 2.0.1 - jsbn@1.1.0: {} - jsdoc-type-pratt-parser@4.0.0: {} jsdoc-type-pratt-parser@4.1.0: {} @@ -17117,7 +17220,6 @@ snapshots: json-bigint@1.0.0: dependencies: bignumber.js: 9.1.2 - optional: true json-buffer@3.0.1: {} @@ -17180,7 +17282,6 @@ snapshots: buffer-equal-constant-time: 1.0.1 ecdsa-sig-formatter: 1.0.11 safe-buffer: 5.2.1 - optional: true jwks-rsa@3.1.0: dependencies: @@ -17202,7 +17303,6 @@ snapshots: dependencies: jwa: 2.0.0 safe-buffer: 5.2.1 - optional: true kdbush@4.0.2: {} @@ -17226,6 +17326,8 @@ snapshots: kolorist@1.8.0: {} + kuler@2.0.0: {} + launch-editor@2.9.1: dependencies: picocolors: 1.0.1 @@ -17345,12 +17447,16 @@ snapshots: lodash@4.17.21: {} - long@5.2.3: {} - - loose-envify@1.4.0: + logform@2.7.0: dependencies: - js-tokens: 4.0.0 - optional: true + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + + long@5.2.3: {} loupe@3.1.1: dependencies: @@ -17368,8 +17474,6 @@ snapshots: dependencies: yallist: 4.0.0 - lru-cache@7.18.3: {} - lru-memoizer@2.3.0: dependencies: lodash.clonedeep: 4.5.0 @@ -17495,17 +17599,22 @@ snapshots: mdurl@2.0.0: {} - media-typer@0.3.0: {} + media-typer@0.3.0: + optional: true + + memory-pager@1.5.0: {} meow@13.2.0: {} - merge-descriptors@1.0.1: {} + merge-descriptors@1.0.1: + optional: true merge-stream@2.0.0: {} merge2@1.4.1: {} - methods@1.1.2: {} + methods@1.1.2: + optional: true micromark@2.11.4: dependencies: @@ -17634,6 +17743,17 @@ snapshots: moment@2.30.1: {} + mongodb-connection-string-url@3.0.2: + dependencies: + '@types/whatwg-url': 11.0.5 + whatwg-url: 14.2.0 + + mongodb@6.17.0: + dependencies: + '@mongodb-js/saslprep': 1.2.2 + bson: 6.10.4 + mongodb-connection-string-url: 3.0.2 + mpd-parser@1.3.0: dependencies: '@babel/runtime': 7.24.4 @@ -17669,7 +17789,7 @@ snapshots: nanoid@3.3.7: {} - nanoid@5.0.7: {} + nanoid@5.1.5: {} nanotar@0.1.1: {} @@ -17677,11 +17797,10 @@ snapshots: natural-compare@1.4.0: {} - negotiator@0.6.3: {} - - netmask@2.0.2: {} + negotiator@0.6.3: + optional: true - nitropack@2.9.7(drizzle-orm@0.32.2(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(pg@8.12.0)(postgres@3.4.4)(react@18.2.0))(encoding@0.1.13)(magicast@0.3.5)(webpack-sources@3.2.3): + nitropack@2.9.7(@upstash/redis@1.35.0)(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4))(encoding@0.1.13)(magicast@0.3.5)(webpack-sources@3.2.3): dependencies: '@cloudflare/kv-asset-handler': 0.3.4 '@netlify/functions': 2.8.1 @@ -17704,7 +17823,7 @@ snapshots: cookie-es: 1.1.0 croner: 8.0.2 crossws: 0.2.4 - db0: 0.1.4(drizzle-orm@0.32.2(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(pg@8.12.0)(postgres@3.4.4)(react@18.2.0)) + db0: 0.1.4(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4)) defu: 6.1.4 destr: 2.0.3 dot-prop: 8.0.2 @@ -17717,7 +17836,7 @@ snapshots: h3: 1.13.0 hookable: 5.5.3 httpxy: 0.1.5 - ioredis: 5.4.1 + ioredis: 5.6.1 jiti: 1.21.6 klona: 2.0.6 knitwork: 1.1.0 @@ -17747,7 +17866,7 @@ snapshots: unctx: 2.3.1(webpack-sources@3.2.3) unenv: 1.10.0 unimport: 3.12.0(rollup@4.22.4)(webpack-sources@3.2.3) - unstorage: 1.12.0(ioredis@5.4.1) + unstorage: 1.16.0(@upstash/redis@1.35.0)(db0@0.1.4(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4)))(ioredis@5.6.1) unwasm: 0.3.9 transitivePeerDependencies: - '@azure/app-configuration' @@ -17757,11 +17876,14 @@ snapshots: - '@azure/keyvault-secrets' - '@azure/storage-blob' - '@capacitor/preferences' + - '@deno/kv' - '@libsql/client' - '@netlify/blobs' - '@planetscale/database' - '@upstash/redis' + - '@vercel/blob' - '@vercel/kv' + - aws4fetch - better-sqlite3 - drizzle-orm - encoding @@ -17769,6 +17891,7 @@ snapshots: - magicast - supports-color - uWebSockets.js + - uploadthing - webpack-sources node-addon-api@5.1.0: {} @@ -17777,6 +17900,8 @@ snapshots: node-fetch-native@1.6.4: {} + node-fetch-native@1.6.6: {} + node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 @@ -17789,7 +17914,7 @@ snapshots: node-releases@2.0.18: {} - nodemailer@6.9.15: {} + nodemailer@6.10.1: {} nopt@5.0.0: dependencies: @@ -17814,6 +17939,11 @@ snapshots: dependencies: path-key: 4.0.0 + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + npmlog@5.0.1: dependencies: are-we-there-yet: 2.0.0 @@ -17852,21 +17982,10 @@ snapshots: - supports-color - webpack-sources - nuxt-module-hotjar@1.3.2(magicast@0.3.5)(rollup@3.29.5)(webpack-sources@3.2.3): - dependencies: - '@hotjar/browser': 1.0.9 - '@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@3.29.5)(webpack-sources@3.2.3) - defu: 6.1.4 - transitivePeerDependencies: - - magicast - - rollup - - supports-color - - webpack-sources - - nuxt-nodemailer@1.1.2(magicast@0.3.5)(nodemailer@6.9.15)(rollup@3.29.5)(webpack-sources@3.2.3): + nuxt-nodemailer@1.1.2(magicast@0.3.5)(nodemailer@6.10.1)(rollup@3.29.5)(webpack-sources@3.2.3): dependencies: '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@3.29.5)(webpack-sources@3.2.3) - nodemailer: 6.9.15 + nodemailer: 6.10.1 transitivePeerDependencies: - magicast - rollup @@ -17888,25 +18007,25 @@ snapshots: - supports-color - webpack-sources - nuxt-vuefire@1.0.3(@firebase/app-types@0.9.2)(firebase-admin@12.2.0(encoding@0.1.13))(firebase-functions@5.0.1(firebase-admin@12.2.0(encoding@0.1.13)))(firebase@10.12.4)(magicast@0.3.5)(rollup@3.29.5)(vuefire@3.1.24(consola@3.4.0)(firebase@10.12.4)(vue@3.5.8(typescript@5.4.5)))(webpack-sources@3.2.3): + nuxt-vuefire@1.0.3(@firebase/app-types@0.9.2)(firebase-admin@13.4.0(encoding@0.1.13))(firebase-functions@5.0.1(firebase-admin@13.4.0(encoding@0.1.13)))(firebase@11.9.0)(magicast@0.3.5)(rollup@3.29.5)(vuefire@3.1.24(consola@3.4.0)(firebase@11.9.0)(vue@3.5.8(typescript@5.4.5)))(webpack-sources@3.2.3): dependencies: '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@3.29.5)(webpack-sources@3.2.3) '@posva/lru-cache': 10.0.1 - firebase: 10.12.4 + firebase: 11.9.0 lodash-es: 4.17.21 strip-json-comments: 5.0.1 - vuefire: 3.1.24(consola@3.4.0)(firebase@10.12.4)(vue@3.5.8(typescript@5.4.5)) + vuefire: 3.1.24(consola@3.4.0)(firebase@11.9.0)(vue@3.5.8(typescript@5.4.5)) optionalDependencies: '@firebase/app-types': 0.9.2 - firebase-admin: 12.2.0(encoding@0.1.13) - firebase-functions: 5.0.1(firebase-admin@12.2.0(encoding@0.1.13)) + firebase-admin: 13.4.0(encoding@0.1.13) + firebase-functions: 5.0.1(firebase-admin@13.4.0(encoding@0.1.13)) transitivePeerDependencies: - magicast - rollup - supports-color - webpack-sources - nuxt@3.13.2(@parcel/watcher@2.4.1)(@types/node@20.12.7)(drizzle-orm@0.32.2(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(pg@8.12.0)(postgres@3.4.4)(react@18.2.0))(encoding@0.1.13)(eslint@8.57.0)(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.3)(rollup@3.29.5)(sass@1.75.0)(stylelint@16.2.1(typescript@5.4.5))(terser@5.30.4)(typescript@5.4.5)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(webpack-sources@3.2.3): + nuxt@3.13.2(@parcel/watcher@2.4.1)(@types/node@20.12.7)(@upstash/redis@1.35.0)(db0@0.1.4(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4)))(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4))(encoding@0.1.13)(eslint@8.57.0)(ioredis@5.6.1)(magicast@0.3.5)(optionator@0.9.3)(rollup@3.29.5)(sass@1.75.0)(stylelint@16.2.1(typescript@5.4.5))(terser@5.30.4)(typescript@5.4.5)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(webpack-sources@3.2.3): dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/devtools': 1.5.1(rollup@3.29.5)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(vue@3.5.8(typescript@5.4.5))(webpack-sources@3.2.3) @@ -17943,7 +18062,7 @@ snapshots: magic-string: 0.30.11 mlly: 1.7.1 nanotar: 0.1.1 - nitropack: 2.9.7(drizzle-orm@0.32.2(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(pg@8.12.0)(postgres@3.4.4)(react@18.2.0))(encoding@0.1.13)(magicast@0.3.5)(webpack-sources@3.2.3) + nitropack: 2.9.7(@upstash/redis@1.35.0)(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4))(encoding@0.1.13)(magicast@0.3.5)(webpack-sources@3.2.3) nuxi: 3.13.2 nypm: 0.3.11 ofetch: 1.3.4 @@ -17966,7 +18085,7 @@ snapshots: unimport: 3.12.0(rollup@3.29.5)(webpack-sources@3.2.3) unplugin: 1.14.1(webpack-sources@3.2.3) unplugin-vue-router: 0.10.8(rollup@3.29.5)(vue-router@4.4.5(vue@3.5.8(typescript@5.4.5)))(vue@3.5.8(typescript@5.4.5))(webpack-sources@3.2.3) - unstorage: 1.12.0(ioredis@5.4.1) + unstorage: 1.16.0(@upstash/redis@1.35.0)(db0@0.1.4(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4)))(ioredis@5.6.1) untyped: 1.4.2 vue: 3.5.8(typescript@5.4.5) vue-bundle-renderer: 2.1.0 @@ -17984,13 +18103,17 @@ snapshots: - '@azure/storage-blob' - '@biomejs/biome' - '@capacitor/preferences' + - '@deno/kv' - '@libsql/client' - '@netlify/blobs' - '@planetscale/database' - '@upstash/redis' + - '@vercel/blob' - '@vercel/kv' + - aws4fetch - better-sqlite3 - bufferutil + - db0 - drizzle-orm - encoding - eslint @@ -18011,6 +18134,7 @@ snapshots: - terser - typescript - uWebSockets.js + - uploadthing - utf-8-validate - vite - vls @@ -18030,8 +18154,7 @@ snapshots: object-assign@4.1.1: {} - object-hash@3.0.0: - optional: true + object-hash@3.0.0: {} object-inspect@1.13.1: {} @@ -18050,6 +18173,12 @@ snapshots: node-fetch-native: 1.6.4 ufo: 1.5.3 + ofetch@1.4.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.6 + ufo: 1.6.1 + ohash@1.1.4: {} on-finished@2.4.1: @@ -18060,6 +18189,10 @@ snapshots: dependencies: wrappy: 1.0.2 + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 @@ -18127,24 +18260,6 @@ snapshots: p-try@2.2.0: {} - pac-proxy-agent@7.0.2: - dependencies: - '@tootallnate/quickjs-emscripten': 0.23.0 - agent-base: 7.1.1 - debug: 4.3.7 - get-uri: 6.0.3 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.5 - pac-resolver: 7.0.1 - socks-proxy-agent: 8.0.4 - transitivePeerDependencies: - - supports-color - - pac-resolver@7.0.1: - dependencies: - degenerator: 5.0.1 - netmask: 2.0.2 - package-json-from-dist@1.0.0: {} package-manager-detector@0.2.0: {} @@ -18230,7 +18345,8 @@ snapshots: lru-cache: 11.0.1 minipass: 7.1.2 - path-to-regexp@0.1.7: {} + path-to-regexp@0.1.7: + optional: true path-type@4.0.0: {} @@ -18587,7 +18703,7 @@ snapshots: pretty-bytes@6.1.1: {} - pretty-ms@9.1.0: + pretty-ms@9.2.0: dependencies: parse-ms: 4.0.0 @@ -18734,19 +18850,7 @@ snapshots: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 - - proxy-agent@6.4.0: - dependencies: - agent-base: 7.1.1 - debug: 4.3.7 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.5 - lru-cache: 7.18.3 - pac-proxy-agent: 7.0.2 - proxy-from-env: 1.1.0 - socks-proxy-agent: 8.0.4 - transitivePeerDependencies: - - supports-color + optional: true proxy-from-env@1.1.0: {} @@ -18759,33 +18863,6 @@ snapshots: punycode@2.3.1: {} - puppeteer-core@23.4.1: - dependencies: - '@puppeteer/browsers': 2.4.0 - chromium-bidi: 0.6.5(devtools-protocol@0.0.1342118) - debug: 4.3.7 - devtools-protocol: 0.0.1342118 - typed-query-selector: 2.12.0 - ws: 8.18.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - puppeteer@23.4.1(typescript@5.4.5): - dependencies: - '@puppeteer/browsers': 2.4.0 - chromium-bidi: 0.6.5(devtools-protocol@0.0.1342118) - cosmiconfig: 9.0.0(typescript@5.4.5) - devtools-protocol: 0.0.1342118 - puppeteer-core: 23.4.1 - typed-query-selector: 2.12.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - typescript - - utf-8-validate - qs@6.11.0: dependencies: side-channel: 1.0.6 @@ -18814,17 +18891,13 @@ snapshots: http-errors: 2.0.0 iconv-lite: 0.4.24 unpipe: 1.0.0 + optional: true rc9@2.1.2: dependencies: defu: 6.1.4 destr: 2.0.3 - react@18.2.0: - dependencies: - loose-envify: 1.4.0 - optional: true - read-pkg-up@7.0.1: dependencies: find-up: 4.1.0 @@ -19034,9 +19107,9 @@ snapshots: dependencies: rust-result: 1.0.0 - safer-buffer@2.1.2: {} + safe-stable-stringify@2.5.0: {} - safevalues@0.6.0: {} + safer-buffer@2.1.2: {} sass@1.75.0: dependencies: @@ -19157,6 +19230,10 @@ snapshots: transitivePeerDependencies: - supports-color + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + sirv@2.0.4: dependencies: '@polka/url': 1.0.0-next.25 @@ -19179,23 +19256,8 @@ snapshots: astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 - smart-buffer@4.2.0: {} - smob@1.5.0: {} - socks-proxy-agent@8.0.4: - dependencies: - agent-base: 7.1.1 - debug: 4.3.7 - socks: 2.8.3 - transitivePeerDependencies: - - supports-color - - socks@2.8.3: - dependencies: - ip-address: 9.0.5 - smart-buffer: 4.2.0 - source-map-js@1.2.0: {} source-map-support@0.5.21: @@ -19207,6 +19269,10 @@ snapshots: source-map@0.7.4: {} + sparse-bitfield@3.0.3: + dependencies: + memory-pager: 1.5.0 + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 @@ -19231,10 +19297,10 @@ snapshots: split2@4.2.0: optional: true - sprintf-js@1.1.3: {} - stable-hash@0.0.4: {} + stack-trace@0.0.10: {} + stackback@0.0.2: {} standard-as-callback@2.1.0: {} @@ -19323,8 +19389,6 @@ snapshots: stubs@3.0.0: optional: true - style-search@0.1.0: {} - stylehacks@7.0.4(postcss@8.4.38): dependencies: browserslist: 4.23.3 @@ -19526,14 +19590,6 @@ snapshots: tapable@2.2.1: {} - tar-fs@3.0.6: - dependencies: - pump: 3.0.0 - tar-stream: 3.1.7 - optionalDependencies: - bare-fs: 2.3.5 - bare-path: 2.1.3 - tar-stream@3.1.7: dependencies: b4a: 1.6.6 @@ -19572,6 +19628,8 @@ snapshots: dependencies: b4a: 1.6.6 + text-hex@1.0.0: {} + text-table@0.2.0: {} thenify-all@1.6.0: @@ -19582,8 +19640,6 @@ snapshots: dependencies: any-promise: 1.3.0 - through@2.3.8: {} - tiny-inflate@1.0.3: {} tiny-invariant@1.3.3: {} @@ -19637,6 +19693,12 @@ snapshots: tr46@0.0.3: {} + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + triple-beam@1.4.1: {} + ts-api-utils@1.3.0(typescript@5.4.5): dependencies: typescript: 5.4.5 @@ -19692,11 +19754,10 @@ snapshots: dependencies: media-typer: 0.3.0 mime-types: 2.1.35 + optional: true type-level-regexp@0.1.17: {} - typed-query-selector@2.12.0: {} - typescript@5.4.5: {} uc.micro@2.1.0: {} @@ -19705,6 +19766,8 @@ snapshots: ufo@1.5.4: {} + ufo@1.6.1: {} + ultrahtml@1.5.3: {} unbuild@2.0.0(sass@1.75.0)(typescript@5.4.5): @@ -19740,11 +19803,6 @@ snapshots: - supports-color - vue-tsc - unbzip2-stream@1.4.3: - dependencies: - buffer: 5.7.1 - through: 2.3.8 - unconfig@0.5.5: dependencies: '@antfu/utils': 0.7.10 @@ -19775,6 +19833,8 @@ snapshots: undici-types@5.26.5: {} + undici-types@6.21.0: {} + undici@5.28.4: dependencies: '@fastify/busboy': 2.1.1 @@ -19806,6 +19866,8 @@ snapshots: unicorn-magic@0.1.0: {} + unicorn-magic@0.3.0: {} + unimport@3.12.0(rollup@3.29.5)(webpack-sources@3.2.3): dependencies: '@rollup/pluginutils': 5.1.0(rollup@3.29.5) @@ -19885,7 +19947,8 @@ snapshots: universalify@2.0.1: {} - unpipe@1.0.0: {} + unpipe@1.0.0: + optional: true unplugin-remove@1.0.3(rollup@3.29.5): dependencies: @@ -19962,20 +20025,20 @@ snapshots: acorn: 8.14.0 webpack-virtual-modules: 0.6.2 - unstorage@1.12.0(ioredis@5.4.1): + unstorage@1.16.0(@upstash/redis@1.35.0)(db0@0.1.4(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4)))(ioredis@5.6.1): dependencies: anymatch: 3.1.3 - chokidar: 3.6.0 - destr: 2.0.3 + chokidar: 4.0.3 + destr: 2.0.5 h3: 1.13.0 - listhen: 1.7.2 lru-cache: 10.4.3 - mri: 1.2.0 - node-fetch-native: 1.6.4 - ofetch: 1.3.4 - ufo: 1.5.4 + node-fetch-native: 1.6.6 + ofetch: 1.4.1 + ufo: 1.6.1 optionalDependencies: - ioredis: 5.4.1 + '@upstash/redis': 1.35.0 + db0: 0.1.4(drizzle-orm@0.40.1(@opentelemetry/api@1.9.0)(@prisma/client@5.19.1)(@types/pg@8.11.8)(gel@2.1.0)(pg@8.12.0)(postgres@3.4.4)) + ioredis: 5.6.1 transitivePeerDependencies: - uWebSockets.js @@ -20003,7 +20066,7 @@ snapshots: '@babel/standalone': 7.26.2 '@babel/types': 7.26.0 defu: 6.1.4 - jiti: 2.4.0 + jiti: 2.4.2 mri: 1.2.0 scule: 1.3.0 transitivePeerDependencies: @@ -20053,16 +20116,17 @@ snapshots: url-toolkit@2.2.5: {} - urlpattern-polyfill@10.0.0: {} - urlpattern-polyfill@8.0.2: {} util-deprecate@1.0.2: {} - utils-merge@1.0.1: {} + utils-merge@1.0.1: + optional: true uuid@10.0.0: {} + uuid@11.1.0: {} + uuid@8.3.2: {} uuid@9.0.1: {} @@ -20072,7 +20136,8 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - vary@1.1.2: {} + vary@1.1.2: + optional: true video.js@8.6.0: dependencies: @@ -20122,6 +20187,23 @@ snapshots: - supports-color - terser + vite-node@2.1.1(@types/node@22.15.30)(sass@1.75.0)(terser@5.30.4): + dependencies: + cac: 6.7.14 + debug: 4.3.7 + pathe: 1.1.2 + vite: 5.2.10(@types/node@22.15.30)(sass@1.75.0)(terser@5.30.4) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + optional: true + vite-plugin-checker@0.8.0(eslint@8.57.0)(optionator@0.9.3)(stylelint@16.2.1(typescript@5.4.5))(typescript@5.4.5)(vite@5.4.7(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4)): dependencies: '@babel/code-frame': 7.24.7 @@ -20180,7 +20262,7 @@ snapshots: vite-plugin-vuetify@2.0.3(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(vue@3.5.8(typescript@5.4.5))(vuetify@3.5.15): dependencies: - '@vuetify/loader-shared': 2.0.3(vue@3.5.8(typescript@5.4.5))(vuetify@3.5.15(typescript@5.4.5)(vite-plugin-vuetify@2.0.3)(vue-i18n@9.13.1(vue@3.5.8(typescript@5.4.5)))(vue@3.5.8(typescript@5.4.5))) + '@vuetify/loader-shared': 2.0.3(vue@3.5.8(typescript@5.4.5))(vuetify@3.5.15) debug: 4.3.7 upath: 2.0.1 vite: 5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4) @@ -20191,7 +20273,7 @@ snapshots: vite-plugin-vuetify@2.0.4(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(vue@3.5.8(typescript@5.4.5))(vuetify@3.7.2): dependencies: - '@vuetify/loader-shared': 2.0.3(vue@3.5.8(typescript@5.4.5))(vuetify@3.7.2(typescript@5.4.5)(vite-plugin-vuetify@2.0.4)(vue@3.5.8(typescript@5.4.5))) + '@vuetify/loader-shared': 2.0.3(vue@3.5.8(typescript@5.4.5))(vuetify@3.7.2) debug: 4.3.7 upath: 2.0.1 vite: 5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4) @@ -20216,6 +20298,18 @@ snapshots: sass: 1.75.0 terser: 5.30.4 + vite@5.2.10(@types/node@22.15.30)(sass@1.75.0)(terser@5.30.4): + dependencies: + esbuild: 0.20.2 + postcss: 8.4.38 + rollup: 4.22.4 + optionalDependencies: + '@types/node': 22.15.30 + fsevents: 2.3.3 + sass: 1.75.0 + terser: 5.30.4 + optional: true + vite@5.4.7(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4): dependencies: esbuild: 0.21.5 @@ -20260,6 +20354,40 @@ snapshots: - supports-color - terser + vitest@2.1.1(@types/node@22.15.30)(sass@1.75.0)(terser@5.30.4): + dependencies: + '@vitest/expect': 2.1.1 + '@vitest/mocker': 2.1.1(@vitest/spy@2.1.1)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4)) + '@vitest/pretty-format': 2.1.1 + '@vitest/runner': 2.1.1 + '@vitest/snapshot': 2.1.1 + '@vitest/spy': 2.1.1 + '@vitest/utils': 2.1.1 + chai: 5.1.1 + debug: 4.3.7 + magic-string: 0.30.11 + pathe: 1.1.2 + std-env: 3.7.0 + tinybench: 2.9.0 + tinyexec: 0.3.0 + tinypool: 1.0.1 + tinyrainbow: 1.2.0 + vite: 5.2.10(@types/node@22.15.30)(sass@1.75.0)(terser@5.30.4) + vite-node: 2.1.1(@types/node@22.15.30)(sass@1.75.0)(terser@5.30.4) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.15.30 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - stylus + - sugarss + - supports-color + - terser + optional: true + vnpay@1.6.0: dependencies: moment-timezone: 0.5.46 @@ -20377,13 +20505,13 @@ snapshots: optionalDependencies: typescript: 5.4.5 - vuefire@3.1.24(consola@3.4.0)(firebase@10.12.4)(vue@3.5.8(typescript@5.4.5)): + vuefire@3.1.24(consola@3.4.0)(firebase@11.9.0)(vue@3.5.8(typescript@5.4.5)): dependencies: vue: 3.5.8(typescript@5.4.5) vue-demi: 0.14.10(vue@3.5.8(typescript@5.4.5)) optionalDependencies: consola: 3.4.0 - firebase: 10.12.4 + firebase: 11.9.0 vuetify-nuxt-module@0.18.3(magicast@0.3.5)(rollup@3.29.5)(typescript@5.4.5)(vite@5.2.10(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4))(vue@3.5.8(typescript@5.4.5))(webpack-sources@3.2.3): dependencies: @@ -20425,8 +20553,12 @@ snapshots: w3c-keyname@2.2.8: {} + web-vitals@4.2.4: {} + webidl-conversions@3.0.1: {} + webidl-conversions@7.0.0: {} + webpack-sources@3.2.3: {} webpack-virtual-modules@0.5.0: {} @@ -20441,6 +20573,11 @@ snapshots: websocket-extensions@0.1.4: {} + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -20458,6 +20595,10 @@ snapshots: dependencies: isexe: 2.0.0 + which@4.0.0: + dependencies: + isexe: 3.1.1 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 @@ -20467,6 +20608,34 @@ snapshots: dependencies: string-width: 4.2.3 + winston-daily-rotate-file@5.0.0(winston@3.17.0): + dependencies: + file-stream-rotator: 0.6.1 + object-hash: 3.0.0 + triple-beam: 1.4.1 + winston: 3.17.0 + winston-transport: 4.9.0 + + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston@3.17.0: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.3 + async: 3.2.5 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 diff --git a/puppeteer.config.mjs b/puppeteer.config.mjs deleted file mode 100644 index 60afe770..00000000 --- a/puppeteer.config.mjs +++ /dev/null @@ -1,9 +0,0 @@ -import path from 'node:path' - -/** - * @type {import("puppeteer").Configuration} - */ -export default { - // Changes the cache location for Puppeteer. - cacheDirectory: path.join(__dirname, './node_modules/.cache', 'puppeteer'), -} diff --git a/server/api/auth/me.get.ts b/server/api/auth/me.get.ts new file mode 100644 index 00000000..db2b1bd2 --- /dev/null +++ b/server/api/auth/me.get.ts @@ -0,0 +1,25 @@ +export default defineEventHandler(async (event) => { + try { + const { session } = await defineEventOptions(event, { auth: true }) + + const { getUserById } = useUser() + + // Get the user data from our database + const user = await tryWithCache(session.id, () => getUserById(session.id)) + + if (!user) { + throw createError({ + statusCode: 404, + message: 'User not found', + }) + } + + return { + data: user, + } + } + catch (error: any) { + logger.error('[User API] Error fetching user:', error) + throw parseError(error) + } +}) diff --git a/server/api/me.patch.ts b/server/api/auth/me.patch.ts similarity index 83% rename from server/api/me.patch.ts rename to server/api/auth/me.patch.ts index 06ac7611..82d82203 100644 --- a/server/api/me.patch.ts +++ b/server/api/auth/me.patch.ts @@ -1,4 +1,5 @@ import { z } from 'zod' +import { cleanDoubleSlashes } from 'ufo' export default defineEventHandler(async (event) => { try { @@ -21,7 +22,7 @@ export default defineEventHandler(async (event) => { const accessToken = await client.getAccessToken() - await $fetch(`${process.env.LOGTO_ENDPOINT}/api/my-account`, { + await $fetch(cleanDoubleSlashes(`${process.env.LOGTO_ENDPOINT}/api/my-account`), { method: 'PATCH', body: { name: body?.name || null, @@ -37,6 +38,8 @@ export default defineEventHandler(async (event) => { return { success: true } } catch (error: any) { + logger.error('[Me API] Error updating user info:', error) + throw parseError(error) } }) diff --git a/server/api/auth/notification.patch.ts b/server/api/auth/notification.patch.ts new file mode 100644 index 00000000..aa12bd2a --- /dev/null +++ b/server/api/auth/notification.patch.ts @@ -0,0 +1,34 @@ +import { z } from 'zod' + +export default defineEventHandler(async (event) => { + try { + const { session } = await defineEventOptions(event, { auth: true }) + + const body = await readValidatedBody( + event, + body => z.object({ + email: z.boolean().nullable(), + desktop: z.boolean().nullable(), + product_updates: z.boolean().nullable(), + weekly_digest: z.boolean().nullable(), + important_updates: z.boolean().nullable(), + }).partial().parse(body), + ) + + const { updateUser } = useUser() + + await updateUser(session.id, { + ...(body.email !== undefined && { email_notifications: body.email }), + ...(body.desktop !== undefined && { desktop_notifications: body.desktop }), + ...(body.product_updates !== undefined && { product_updates_notifications: body.product_updates }), + ...(body.weekly_digest !== undefined && { weekly_digest_notifications: body.weekly_digest }), + ...(body.important_updates !== undefined && { important_updates_notifications: body.important_updates }), + }) + + return { success: true } + } + catch (error: any) { + logger.error('[Notification API] Error updating user notification settings:', error) + throw parseError(error) + } +}) diff --git a/server/api/auth/password.post.ts b/server/api/auth/password.post.ts new file mode 100644 index 00000000..cd7899c8 --- /dev/null +++ b/server/api/auth/password.post.ts @@ -0,0 +1,44 @@ +import { z } from 'zod' +import { cleanDoubleSlashes } from 'ufo' + +export default defineEventHandler(async (event) => { + try { + await defineEventOptions(event, { auth: true }) + + const client = useLogtoClient() + + const body = await readValidatedBody( + event, + body => z.object({ + password: z.string(), + password_new: z.string(), + }).parse(body), + ) + + const accessToken = await client.getAccessToken() + + const verification = await $fetch<{ verificationRecordId: string }>(cleanDoubleSlashes(`${process.env.LOGTO_ENDPOINT}/api/verifications/password`), { + method: 'POST', + body: { password: body.password }, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + + await $fetch(cleanDoubleSlashes(`${process.env.LOGTO_ENDPOINT}/api/my-account/password`), { + method: 'POST', + body: { password: body.password_new }, + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'logto-verification-id': verification.verificationRecordId, + }, + }) + + return { success: true } + } + catch (error: any) { + logger.error('[Password API] Error updating user password:', error) + + throw parseError(error) + } +}) diff --git a/server/api/devices/index.delete.ts b/server/api/devices/index.delete.ts new file mode 100644 index 00000000..642447b4 --- /dev/null +++ b/server/api/devices/index.delete.ts @@ -0,0 +1,18 @@ +export default defineEventHandler(async (event) => { + try { + const { session } = await defineEventOptions(event, { auth: true }) + + const { token } = await readBody(event) + + const { deleteDeviceToken } = useDeviceToken() + + await deleteDeviceToken(session.id, token) + + return { message: 'Token unregistration successful' } + } + catch (error: any) { + logger.error('[Device API] Error unregistering device token:', error) + + throw parseError(error) + } +}) diff --git a/server/api/devices/index.post.ts b/server/api/devices/index.post.ts new file mode 100644 index 00000000..86fd7c96 --- /dev/null +++ b/server/api/devices/index.post.ts @@ -0,0 +1,26 @@ +import { z } from 'zod' + +export default defineEventHandler(async (event) => { + try { + const { session } = await defineEventOptions(event, { auth: true }) + + const { token } = await readValidatedBody(event, z.object({ token: z.string() }).parse) + + const { getDeviceToken, createDeviceToken } = useDeviceToken() + + const existingDeviceToken = await getDeviceToken(session.id, token) + + if (!existingDeviceToken) { + const createdToken = await createDeviceToken(session.id, token) + + return { message: 'Token registration successful', token: createdToken.token_device } + } + + return { message: 'Token registration successful' } + } + catch (error: any) { + logger.error('[Device API] Error registering device token:', error) + + throw parseError(error) + } +}) diff --git a/server/api/faq.get.ts b/server/api/faq.get.ts deleted file mode 100644 index 75fe03e9..00000000 --- a/server/api/faq.get.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useFaqCrud } from '@base/server/composables/useFaqCrud' - -export default defineEventHandler(async (event) => { - const { getFaqQuestions } = useFaqCrud() - - const faqs = await getFaqQuestions(getFilter(event)) - setResponseStatus(event, 200) - - return faqs -}) diff --git a/server/api/firebase.post.ts b/server/api/firebase.post.ts index 6120e33d..6656d3e8 100644 --- a/server/api/firebase.post.ts +++ b/server/api/firebase.post.ts @@ -1,5 +1,4 @@ import admin from 'firebase-admin' -import { useUserDeviceCrud } from '@base/server/composables/useUserDeviceCrud' import { z } from 'zod' export default defineEventHandler(async (event) => { @@ -13,19 +12,20 @@ export default defineEventHandler(async (event) => { if (admin.apps.length === 0) { admin.initializeApp({ - credential: admin.credential.cert(service), + credential: admin.credential.cert(service as admin.ServiceAccount), }) } - const { getUserDeviceAllTokens } = useUserDeviceCrud({ user_id }) - const response = await getUserDeviceAllTokens({} as ParsedFilterQuery) + const { getDeviceTokens } = useDeviceToken() - if (response && response.total === 0) { + const response = await getDeviceTokens(user_id) + + if (response && response.length === 0) { setResponseStatus(event, 200) return { message: 'No device found' } } else { - const tokens = response.data!.map((item: any) => item.token_device) + const tokens = response.map((item: any) => item.token_device) const body = { tokens, notification: { @@ -47,6 +47,8 @@ export default defineEventHandler(async (event) => { } } catch (error: any) { + logger.error('[Firebase API] Error sending notification:', error) + throw parseError(error) } }) diff --git a/server/api/logto/webhook.post.ts b/server/api/logto/webhook.post.ts new file mode 100644 index 00000000..ec17b1ff --- /dev/null +++ b/server/api/logto/webhook.post.ts @@ -0,0 +1,283 @@ +import { createHmac } from 'node:crypto' +import { eq } from 'drizzle-orm' +import { userTable } from '@base/server/db/schemas' + +// Define Logto event types +type LogtoEventType = + | 'PostRegister' + | 'PostSignIn' + | 'User.Created' + | 'User.Deleted' + | 'User.Data.Updated' + | 'User.SuspensionStatus.Updated' + +// Define Logto webhook payload interface +interface LogtoWebhookPayload { + hookId: string + event: LogtoEventType + createdAt: string + userAgent?: string + ip?: string + userId?: string + user?: LogtoUserEntity + data?: LogtoUserEntity | null + params?: { id?: string, userId?: string } + interactionEvent?: string + sessionId?: string + applicationId?: string +} + +// Define Logto user entity interface +interface LogtoUserEntity { + id: string + username?: string + primaryEmail?: string + primaryPhone?: string + name?: string + avatar?: string + customData?: Record + identities?: Record + lastSignInAt?: string + createdAt?: string + applicationId?: string + isSuspended?: boolean +} + +// Define Logto identity interface +interface LogtoIdentity { + userId: string + details?: { + id?: string + email?: string + name?: string + avatar?: string + phone?: string + } +} + +// Get signing key from environment variable or config +const LOGTO_SIGNING_KEY = process.env.LOGTO_WEBHOOK_SIGNING_KEY || '' + +// Verify the Logto webhook signature +// eslint-disable-next-line node/prefer-global/buffer +function verifySignature(signingKey: string, rawBody: Buffer, signature: string): boolean { + if (!signingKey) + return false + + const hmac = createHmac('sha256', signingKey) + hmac.update(rawBody) + const computedSignature = hmac.digest('hex') + + return computedSignature === signature +} + +// Map Logto user data to our user schema +function mapLogtoUserToUserInput(logtoUser: LogtoUserEntity) { + return { + logto_id: logtoUser.id, + username: logtoUser.username, + name: logtoUser.name, + primary_email: logtoUser.primaryEmail, + primary_phone: logtoUser.primaryPhone, + avatar: logtoUser.avatar, + custom_data: logtoUser.customData, + last_sign_in_at: logtoUser.lastSignInAt ? new Date(logtoUser.lastSignInAt) : undefined, + is_suspended: logtoUser.isSuspended, + } +} + +// Process identities from Logto user data +async function processIdentities(userId: string, identities?: Record) { + if (!identities) + return + + const { upsertIdentity } = useIdentity() + + // Process each identity provider + for (const [provider, identity] of Object.entries(identities)) { + const providerUserId = identity?.details?.id || '' + if (!providerUserId) + continue + + // If we've made it here, we know identity.details exists + // Map the provider data from the identity details + const providerData = { + email: identity.details!.email, + name: identity.details!.name, + avatar: identity.details!.avatar, + phone: identity.details!.phone, + } + + // Create or update the identity entry + await upsertIdentity(userId, provider, providerUserId, providerData) + } +} + +export default defineEventHandler(async (event) => { + try { + // Get the raw request body as buffer for signature verification + // Use false parameter to get the raw buffer instead of parsed string + const rawBody = await readRawBody(event, false) + if (!rawBody) { + return sendError(event, createError({ + statusCode: 400, + statusMessage: 'Invalid request body', + })) + } + + // Get the signature from headers + const signature = getRequestHeader(event, 'logto-signature-sha-256') + if (!signature) { + return sendError(event, createError({ + statusCode: 400, + statusMessage: 'Missing signature header', + })) + } + + // Verify the signature with the raw body buffer + if (!verifySignature(LOGTO_SIGNING_KEY, rawBody, signature)) { + return sendError(event, createError({ + statusCode: 401, + statusMessage: 'Invalid signature', + })) + } + + // Parse the request body JSON + const body = JSON.parse(rawBody.toString()) as LogtoWebhookPayload + const { event: eventType, user, data, userId, params } = body + + // Get user composables + const { upsertUser, updateLastSignIn, updateSuspensionStatus, deleteUser } = useUser() + const { deleteIdentitiesByUserId } = useIdentity() + + // Get reference composables + const { createReference, deleteReferenceByUserId } = useReference() + + // Handle different event types + switch (eventType) { + case 'PostSignIn': { + if (!user || !userId) { + return sendError(event, createError({ + statusCode: 400, + statusMessage: 'Missing user data for PostSignIn event', + })) + } + + // Create or update user data and update last sign-in time + const updatedUser = await upsertUser(userId, mapLogtoUserToUserInput(user)) + + // Process any identity information + await processIdentities(updatedUser.id, user.identities) + + // Update last sign-in time + await updateLastSignIn(updatedUser.id) + break + } + + case 'User.Created': { + if (!data?.id) { + return sendError(event, createError({ + statusCode: 400, + statusMessage: 'Missing user data for User.Created event', + })) + } + + // Create or update user + const createdUser = await upsertUser(data.id, mapLogtoUserToUserInput(data)) + await createReference({ userId: createdUser.id, percentage: 5, amount: 0}); + // Process any identity information + await processIdentities(createdUser.id, data.identities) + break + } + + case 'User.Deleted': { + const userId = params?.userId || data?.id + + if (!userId) { + return sendError(event, createError({ + statusCode: 400, + statusMessage: 'Missing userId for User.Deleted event', + })) + } + + // Find user by Logto ID + const user = await db.query.userTable.findFirst({ + where: eq(userTable.logto_id, userId), + }) + + if (user) { + // Delete identities first to maintain referential integrity + await deleteIdentitiesByUserId(user.id) + + // Then delete the user + await deleteUser(user.id) + + // Finally, delete any references associated with the user + await deleteReferenceByUserId(user.id) + } + break + } + + case 'User.Data.Updated': { + const userId = params?.userId || data?.id + + if (!data || !userId) { + return sendError(event, createError({ + statusCode: 400, + statusMessage: 'Missing user data for User.Data.Updated event', + })) + } + + // Update user data + const updatedUser = await upsertUser(userId, mapLogtoUserToUserInput(data)) + + // Process any identity information + await processIdentities(updatedUser.id, data.identities) + break + } + + case 'User.SuspensionStatus.Updated': { + const userId = params?.userId || data?.id + + if (!data || !userId) { + return sendError(event, createError({ + statusCode: 400, + statusMessage: 'Missing user data for User.SuspensionStatus.Updated event', + })) + } + + // Find user by Logto ID to get internal UUID + const user = await db.query.userTable.findFirst({ + where: eq(userTable.logto_id, userId), + }) + + if (user && data.isSuspended !== undefined) { + await updateSuspensionStatus(user.id, data.isSuspended) + } + break + } + + default: { + // Ignore other event types + console.log(`Unhandled Logto webhook event: ${eventType}`) + } + } + + // Return a success response + return { success: true, event: eventType } + } + catch (error: any) { + console.error('Error processing Logto webhook:', error) + + // If it's already a Nuxt error, just rethrow it + if (error.statusCode) { + throw error + } + + // Otherwise create a generic error + return sendError(event, createError({ + statusCode: 500, + statusMessage: error.message || 'Internal server error', + })) + } +}) diff --git a/server/api/notifications/[notificationUId]/index.delete.ts b/server/api/notifications/[notificationUId]/index.delete.ts new file mode 100644 index 00000000..19afb036 --- /dev/null +++ b/server/api/notifications/[notificationUId]/index.delete.ts @@ -0,0 +1,14 @@ +export default defineEventHandler(async (event) => { + try { + const { session, notificationUId } = await defineEventOptions(event, { auth: true, params: ['notificationUId'] }) + + const { deleteNotificationById } = useNotification() + + return deleteNotificationById(notificationUId, session.id) + } + catch (error: any) { + logger.error('[Notification API] Error deleting notification:', error) + + throw parseError(error) + } +}) diff --git a/server/api/notifications/[notificationUId]/read.patch.ts b/server/api/notifications/[notificationUId]/read.patch.ts new file mode 100644 index 00000000..932957fa --- /dev/null +++ b/server/api/notifications/[notificationUId]/read.patch.ts @@ -0,0 +1,14 @@ +export default defineEventHandler(async (event) => { + try { + const { session, notificationUId } = await defineEventOptions(event, { auth: true, params: ['notificationUId'] }) + + const { readNotificationById } = useNotification() + + return readNotificationById(notificationUId, session.id) + } + catch (error: any) { + logger.error('[Notification API] Error marking notification as read:', error) + + throw parseError(error) + } +}) diff --git a/server/api/notifications/[notificationUId]/unread.patch.ts b/server/api/notifications/[notificationUId]/unread.patch.ts new file mode 100644 index 00000000..1d7c8acc --- /dev/null +++ b/server/api/notifications/[notificationUId]/unread.patch.ts @@ -0,0 +1,14 @@ +export default defineEventHandler(async (event) => { + try { + const { session, notificationUId } = await defineEventOptions(event, { auth: true, params: ['notificationUId'] }) + + const { unreadNotificationById } = useNotification() + + return unreadNotificationById(notificationUId, session.id) + } + catch (error: any) { + logger.error('[Notification API] Error marking notification as unread:', error) + + throw parseError(error) + } +}) diff --git a/server/api/notifications/index.get.ts b/server/api/notifications/index.get.ts new file mode 100644 index 00000000..d62922af --- /dev/null +++ b/server/api/notifications/index.get.ts @@ -0,0 +1,15 @@ +export default defineEventHandler(async (event) => { + try { + const { session } = await defineEventOptions(event, { auth: true }) + + const { getNotificationsPaginated } = useNotification() + + const result = await getNotificationsPaginated(session.id, getFilter(event)) + return result.data + } + catch (error: any) { + logger.error('[Notification API] Error fetching notifications:', error) + + throw parseError(error) + } +}) diff --git a/server/api/notifications/read.patch.ts b/server/api/notifications/read.patch.ts new file mode 100644 index 00000000..b654822e --- /dev/null +++ b/server/api/notifications/read.patch.ts @@ -0,0 +1,14 @@ +export default defineEventHandler(async (event) => { + try { + const { session } = await defineEventOptions(event, { auth: true }) + + const { markAllRead } = useNotification() + + return markAllRead(session.id) + } + catch (error: any) { + logger.error('[Notification API] Error marking all notifications as read:', error) + + throw parseError(error) + } +}) diff --git a/server/api/notifications/unread.get.ts b/server/api/notifications/unread.get.ts new file mode 100644 index 00000000..ad8fb11a --- /dev/null +++ b/server/api/notifications/unread.get.ts @@ -0,0 +1,14 @@ +export default defineEventHandler(async (event) => { + try { + const { session } = await defineEventOptions(event, { auth: true }) + + const { getUnreadNotification } = useNotification() + + return getUnreadNotification(session.id) + } + catch (error: any) { + logger.error('[Notification API] Error fetching unread notifications:', error) + + throw parseError(error) + } +}) diff --git a/server/api/notifications/unread.patch.ts b/server/api/notifications/unread.patch.ts new file mode 100644 index 00000000..0ec65252 --- /dev/null +++ b/server/api/notifications/unread.patch.ts @@ -0,0 +1,14 @@ +export default defineEventHandler(async (event) => { + try { + const { session } = await defineEventOptions(event, { auth: true }) + + const { markAllUnread } = useNotification() + + return markAllUnread(session.id) + } + catch (error: any) { + logger.error('[Notification API] Error marking all notifications as unread:', error) + + throw parseError(error) + } +}) diff --git a/server/api/payments/payos/callback.get.ts b/server/api/payments/payos/callback.get.ts deleted file mode 100644 index f8531026..00000000 --- a/server/api/payments/payos/callback.get.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { eq } from 'drizzle-orm' -import { PaymentStatus, paymentProviderTransactionTable, userPaymentTable } from '@base/server/db/schemas' -import { joinURL, withQuery } from 'ufo' - -export default defineEventHandler(async (event) => { - const runtimeConfig = useRuntimeConfig() - - try { - const { id } = getQuery(event) - const { session } = await defineEventOptions(event, { - auth: true, - }) - - if (!id) - throw new Error('Invalid Params') - - const { status, transactions, orderCode } = await payOSAdmin.getPaymentLinkInformation(id as string) - - const { user_payments, payment_provider_transactions } = (await db.select() - .from(paymentProviderTransactionTable) - .where(eq(paymentProviderTransactionTable.provider_transaction_id, orderCode.toString())) - .innerJoin(userPaymentTable, eq(userPaymentTable.id, paymentProviderTransactionTable.payment_id)))[0] - - if (!user_payments) - throw new Error('Order Not Found') - - if (payment_provider_transactions.provider_transaction_status !== PaymentStatus.PENDING) - throw new Error('Order Already Confirmed') - - if (status === 'PENDING') - throw new Error('Invalid Status') - - const transactionStatus = status === 'PAID' ? PaymentStatus.RESOLVED : PaymentStatus.FAILED - - await db.transaction(async (db) => { - const date = new Date(transactions[0].transactionDateTime) - await db.update(paymentProviderTransactionTable).set({ - provider_transaction_status: transactionStatus, - provider_transaction_resolved_at: date, - }).where(eq(paymentProviderTransactionTable.id, payment_provider_transactions.id)) - - await db.update(userPaymentTable).set({ - status: transactionStatus, - }).where(eq(userPaymentTable.id, user_payments.id)) - }) - - if (transactions[0].description.includes('credit') && transactionStatus === PaymentStatus.RESOLVED) { - const [_, amount] = payment_provider_transactions.provider_transaction_info.split(':') - await addCreditToUser(session, Number.parseInt(amount)) - } - - // TODO: Do something with the success - - return sendRedirect( - event, - withQuery( - joinURL( - runtimeConfig.public.appBaseUrl, - runtimeConfig.public.appPaymentRedirect, - ), - { paymentStatus: transactionStatus === PaymentStatus.RESOLVED ? 'success' : 'fail' }, - ), - 200, - ) - } - catch { - return sendRedirect( - event, - withQuery( - joinURL( - runtimeConfig.public.appBaseUrl, - runtimeConfig.public.appPaymentRedirect, - ), - { paymentStatus: 'fail' }, - ), - 200, - ) - } -}) diff --git a/server/api/payments/payos/cancel.get.ts b/server/api/payments/payos/cancel.get.ts deleted file mode 100644 index 60d8d31a..00000000 --- a/server/api/payments/payos/cancel.get.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { eq } from 'drizzle-orm' -import { PaymentStatus, paymentProviderTransactionTable, userPaymentTable } from '@base/server/db/schemas' -import { joinURL, withQuery } from 'ufo' - -export default defineEventHandler(async (event) => { - const runtimeConfig = useRuntimeConfig() - - try { - const { code, cancel, status, orderCode } = getQuery(event) - - if (code !== '00' || !orderCode) - throw new Error('Invalid Params') - - if (cancel === 'true' && status === 'CANCELLED') { - const { user_payments, payment_provider_transactions } = (await db.select() - .from(paymentProviderTransactionTable) - .where(eq(paymentProviderTransactionTable.provider_transaction_id, orderCode.toString())) - .innerJoin(userPaymentTable, eq(userPaymentTable.id, paymentProviderTransactionTable.payment_id)))[0] - - if (!user_payments) - throw new Error('Order Not Found') - - if (payment_provider_transactions.provider_transaction_status === PaymentStatus.CANCELLED) - throw new Error('Order Already Cancelled') - - await db.transaction(async (db) => { - const date = new Date() - await db.update(paymentProviderTransactionTable).set({ - provider_transaction_status: PaymentStatus.CANCELLED, - provider_transaction_resolved_at: date, - }).where(eq(paymentProviderTransactionTable.id, payment_provider_transactions.id)) - - await db.update(userPaymentTable).set({ - status: PaymentStatus.CANCELLED, - }).where(eq(userPaymentTable.id, user_payments.id)) - }) - } - const runtimeConfig = useRuntimeConfig() - // TODO: Do something with the cancel - - return sendRedirect( - event, - withQuery( - joinURL( - runtimeConfig.public.appBaseUrl, - runtimeConfig.public.appPaymentRedirect, - ), - { paymentStatus: 'cancelled' }, - ), - 200, - ) - } - catch { - return sendRedirect( - event, - withQuery( - joinURL( - runtimeConfig.public.appBaseUrl, - runtimeConfig.public.appPaymentRedirect, - ), - { paymentStatus: 'cancelled' }, - ), - 200, - ) - } -}) diff --git a/server/api/payments/payos/checkout.post.ts b/server/api/payments/payos/checkout.post.ts index 8318cfcf..ad3ddb6f 100644 --- a/server/api/payments/payos/checkout.post.ts +++ b/server/api/payments/payos/checkout.post.ts @@ -1,22 +1,22 @@ export default defineEventHandler(async (event) => { try { - const { productIdentifier } = await readBody(event) const { session } = await defineEventOptions(event, { auth: true }) + const { productIdentifier } = await readBody(event) const paymentUrl = await createPaymentCheckout('payos', { productIdentifier, user: session, }) - setResponseStatus(event, 200) return { data: { - message: 'Success', paymentUrl, }, } } catch (error: any) { + logger.error('[Payment API] Error creating Payos checkout URL:', error) + throw parseError(error) } }) diff --git a/server/api/payments/payos/query.post.ts b/server/api/payments/payos/query.post.ts deleted file mode 100644 index d985f7ac..00000000 --- a/server/api/payments/payos/query.post.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { paymentProviderTransactionTable, userPaymentTable } from '@base/server/db/schemas' -import { eq } from 'drizzle-orm' - -export default defineEventHandler(async (event) => { - try { - const { paymentId } = await readBody(event) - - const { user_payments, payment_provider_transactions } = (await db.select() - .from(userPaymentTable) - .where(eq(userPaymentTable.id, paymentId)) - .innerJoin(paymentProviderTransactionTable, eq(paymentProviderTransactionTable.payment_id, userPaymentTable.id)))[0] - - if (!user_payments || !payment_provider_transactions) { - throw new Error('Payment not found') - } - - const data = await payOSAdmin.getPaymentLinkInformation(payment_provider_transactions.provider_transaction_id as string) - - setResponseStatus(event, 200) - return { - message: 'Success', - data, - } - } - catch (error: any) { - throw parseError(error) - } -}) diff --git a/server/api/payments/payos/webhook.post.ts b/server/api/payments/payos/webhook.post.ts index 47f8af81..b7118ed4 100644 --- a/server/api/payments/payos/webhook.post.ts +++ b/server/api/payments/payos/webhook.post.ts @@ -1,56 +1,81 @@ -import { eq } from 'drizzle-orm' -import { PaymentStatus, paymentProviderTransactionTable, userPaymentTable } from '@base/server/db/schemas' +import { PaymentStatus } from '@base/server/db/schemas' export default defineEventHandler(async (event) => { try { - const data = await readBody(event) - const isValid = payOSAdmin.verifyPaymentWebhookData(data) + const body = await readBody(event) - if (!isValid) { - throw new Error(ErrorMessage.INVALID_BODY) + logger.log('[PayOS Webhook] Received webhook data:', body) + + const webhookData = getPayOSAdmin().verifyPaymentWebhookData(body) + + if (!webhookData) { + logger.error('[PayOS Webhook] Invalid webhook data received:', body) + throw createError({ + statusCode: 400, + message: ErrorMessage.INVALID_WEBHOOK_BODY, + data: body, + }) } - const { code, orderCode, transactionDateTime } = data + logger.log('[PayOS Webhook] Verified webhook data:', webhookData) + const transactionStatus = webhookData.code === '00' ? PaymentStatus.RESOLVED : PaymentStatus.FAILED - const { user_payments, payment_provider_transactions } = (await db.select() - .from(paymentProviderTransactionTable) - .where(eq(paymentProviderTransactionTable.provider_transaction_id, orderCode.toString())) - .innerJoin(userPaymentTable, eq(userPaymentTable.id, paymentProviderTransactionTable.payment_id)))[0] + const { updatePaymentStatus, updateProviderTransactionStatus, getProviderTransactionByOrderCode } = usePayment() - if (!user_payments) { - setResponseStatus(event, 200) - return { - success: true, - } + const paymentTransactionOfProvider = await getProviderTransactionByOrderCode(String(webhookData.orderCode)) + + if (!paymentTransactionOfProvider?.payment.order.package) { + logger.warn(`[PayOS Webhook] Transaction not found or invalid: orderCode=${webhookData.orderCode}`) + return { success: true } } - if (payment_provider_transactions.provider_transaction_status !== PaymentStatus.PENDING) { - setResponseStatus(event, 200) - return { - success: true, - } + logger.log(`[PayOS Webhook] Processing transaction: orderCode=${webhookData.orderCode}, status=${transactionStatus}`) + + const priceDiscount = Number(paymentTransactionOfProvider.payment.order.package.price_discount) + const price = Number(paymentTransactionOfProvider.payment.order.package.price) + + if (priceDiscount !== Number(webhookData.amount) && price !== Number(webhookData.amount)) { + logger.error(`[PayOS Webhook] Amount mismatch, transaction [${paymentTransactionOfProvider.id}]: expected=${price}, received=${webhookData.amount}`) + + throw createError({ + statusCode: 400, + message: 'Amount mismatch!', + }) } - const transactionStatus = code === '00' ? PaymentStatus.RESOLVED : PaymentStatus.FAILED + const creditAmount = Number(paymentTransactionOfProvider.payment.order.package.amount) + const userId = paymentTransactionOfProvider.payment.order.user_id + + // The userId is already the UUID from our database since we've updated + // our schemas to use UUID references between tables + logger.log(`[PayOS Webhook] Adding credits: userId=${userId}, amount=${creditAmount}`) - await db.transaction(async (db) => { - const date = new Date(transactionDateTime) - await db.update(paymentProviderTransactionTable).set({ - provider_transaction_status: transactionStatus, - provider_transaction_resolved_at: date, - }).where(eq(paymentProviderTransactionTable.id, payment_provider_transactions.id)) + await addCreditToUser(userId, creditAmount) - await db.update(userPaymentTable).set({ - status: transactionStatus, - }).where(eq(userPaymentTable.id, user_payments.id)) - }) + logger.log(`[PayOS Webhook] Credits added successfully: userId=${userId}, amount=${creditAmount}`) - setResponseStatus(event, 200) - return { - success: true, + if (!paymentTransactionOfProvider?.payment.order.package) { + logger.error(`[PayOS Webhook] No product found for transaction: ${webhookData.orderCode}`) + throw createError({ + statusCode: 400, + message: 'No product found for this transaction!', + }) } + + logger.log(`[PayOS Webhook] Updating transaction ${paymentTransactionOfProvider.id} to status: ${transactionStatus}`) + + await updateProviderTransactionStatus(paymentTransactionOfProvider.id, transactionStatus, webhookData.transactionDateTime) + + await updatePaymentStatus(paymentTransactionOfProvider.payment.id, transactionStatus) + + logger.log(`[PayOS Webhook] Transaction updated successfully: id=${paymentTransactionOfProvider.id}, status=${transactionStatus}`) + + logger.log('[PayOS Webhook] Webhook processing completed successfully') + return { success: true } } catch (error: any) { + logger.error('[PayOS Webhook] Error processing webhook:', error) + throw parseError(error) } }) diff --git a/server/api/payments/vnpay/checkout.post.ts b/server/api/payments/sepay/checkout.post.ts similarity index 67% rename from server/api/payments/vnpay/checkout.post.ts rename to server/api/payments/sepay/checkout.post.ts index 681a2078..65cab6a2 100644 --- a/server/api/payments/vnpay/checkout.post.ts +++ b/server/api/payments/sepay/checkout.post.ts @@ -1,24 +1,22 @@ export default defineEventHandler(async (event) => { try { - const clientIP = getRequestIP(event) const { session } = await defineEventOptions(event, { auth: true }) const { productIdentifier } = await readBody(event) - const paymentUrl = await createPaymentCheckout('vnpay', { - clientIP, + const paymentUrl = await createPaymentCheckout('sepay', { productIdentifier, user: session, }) - setResponseStatus(event, 200) return { data: { - message: 'Success', paymentUrl, }, } } catch (error: any) { + logger.error('[Payment API] Error creating SePay checkout URL:', error) + throw parseError(error) } }) diff --git a/server/api/payments/sepay/status.get.ts b/server/api/payments/sepay/status.get.ts new file mode 100644 index 00000000..147ed82b --- /dev/null +++ b/server/api/payments/sepay/status.get.ts @@ -0,0 +1,40 @@ +import { z } from 'zod' + +import { PaymentStatus } from '@base/server/db/schemas' + +export default defineEventHandler(async (event) => { + try { + await defineEventOptions(event, { auth: true }) + + const { description } = await getValidatedQuery( + event, + query => z.object({ + description: z.string().min(1, 'Payment description must be in the correct format!'), + }) + .refine((query) => { + const orderCode = query.description.slice(2) + + return orderCode.length === 16 + }, { message: 'Payment description must be in the correct format!' }) + .parse(query), + ) + + // remove the first 2 letters + const orderCode = description.slice(2) + + const { getProviderTransactionByOrderCode } = usePayment() + + const paymentTransactionOfProvider = await getProviderTransactionByOrderCode(String(orderCode)) + + return { + data: { + status: paymentTransactionOfProvider?.provider_transaction_status || PaymentStatus.PENDING, + }, + } + } + catch (error: any) { + logger.error('[Payment API] Error creating SePay checkout URL:', error) + + throw parseError(error) + } +}) diff --git a/server/api/payments/sepay/webhook.post.ts b/server/api/payments/sepay/webhook.post.ts new file mode 100644 index 00000000..982b7ec4 --- /dev/null +++ b/server/api/payments/sepay/webhook.post.ts @@ -0,0 +1,125 @@ +import { PaymentStatus } from '@base/server/db/schemas' +import { z } from 'zod' + +export default defineEventHandler(async (event) => { + try { + const body = await readValidatedBody( + event, + payload => z.object({ + accountNumber: z.string(), // e.g., "17228427" + accumulated: z.number(), // e.g., 0 + code: z.string(), // e.g., "SPN8NHOSTING123" + content: z.string(), // e.g., "MBVCB.9604208212.518683.SPN8NHOSTING123.CT tu 0041000331568 NGUYEN HUU NGUYEN Y toi 17228427 NGUYEN HUU NGUYEN Y tai ACB GD 518683-052325 23:34:40" + description: z.string(), // e.g., "BankAPINotify MBVCB.9604208212.518683.SPN8NHOSTING123.CT tu 0041000331568 NGUYEN HUU NGUYEN Y toi 17228427 NGUYEN HUU NGUYEN Y tai ACB GD 518683-052325 23:34:40" + gateway: z.string(), // e.g., "ACB" + id: z.number(), // e.g., 13425123 + referenceCode: z.string(), // e.g., "3165" + subAccount: z.string().optional().nullable(), // e.g., null + transactionDate: z.string(), // e.g., "2025-05-23 23:34:40" + transferAmount: z.number(), // e.g., 2000 + transferType: z.string(), // e.g., "in" + }).parse(payload), + ) + + if (process.env.SEPAY_WEBHOOK_SIGNING_KEY !== getHeader(event, 'Authorization')?.match(/Apikey (.*)/)?.[1]) { + logger.error('[SePay Webhook] Invalid webhook authentication') + throw createError({ + statusCode: 401, + message: 'Invalid webhook authentication', + }) + } + + logger.log('[SePay Webhook] Verified webhook data:', body) + + // SePay Webhook always success (if not, it will not call this endpoint anyway) + + const transactionStatus = PaymentStatus.RESOLVED + const orderCode = body.code.slice(2) || '' // Remove the first 2 characters (SP) + + const { updatePaymentStatus, updateProviderTransactionStatus, getProviderTransactionByOrderCode } = usePayment() + + const paymentTransactionOfProvider = await getProviderTransactionByOrderCode(String(orderCode)) + + if (!paymentTransactionOfProvider?.payment.order.package) { + logger.warn(`[SePay Webhook] Transaction not found or invalid: code=${orderCode}`) + return { success: true } + } + + logger.log(`[SePay Webhook] Processing transaction: code=${orderCode}, status=${transactionStatus}`) + + const userId = paymentTransactionOfProvider.payment.order.user_id + + const { getUserBestPrice, createReferenceUsage } = useReference() + const { getReferenceDiscountAmountForUser } = useOrder() + const reference = paymentTransactionOfProvider.payment.order.reference + const order = paymentTransactionOfProvider.payment.order + + const price = await getUserBestPrice( + userId, + Number(paymentTransactionOfProvider.payment.order.package.price), + Number(paymentTransactionOfProvider.payment.order.package.price_discount), + reference?.code, + ) + + if (price !== Number(body.transferAmount)) { + logger.error(`[SePay Webhook] Amount mismatch, transaction [${paymentTransactionOfProvider.id}]: expected=${price}, received=${body.transferAmount}`) + + throw createError({ + statusCode: 400, + message: 'Amount mismatch!', + }) + } + + const creditAmount = Number(paymentTransactionOfProvider.payment.order.package.amount) + + // The userId is already the UUID from our database since we've updated + // our schemas to use UUID references between tables + logger.log(`[SePay Webhook] Adding credits: userId=${userId}, amount=${creditAmount}`) + + await addCreditToUser(userId, creditAmount) + + logger.log(`[SePay Webhook] Credits added successfully: userId=${userId}, amount=${creditAmount}`) + + + // Affiliate + if(reference?.code) { + const affiliateAmount = await getReferenceDiscountAmountForUser(order.id, userId) + + logger.log(`[SePay Webhook] Adding credits: userId=${reference?.user_id}, amount=${affiliateAmount}`) + await addCreditToUser(reference?.user_id, affiliateAmount) + logger.log(`[SePay Webhook] Credits added successfully: userId=${reference?.user_id}, amount=${affiliateAmount}`) + } + + if (!paymentTransactionOfProvider?.payment.order.package) { + logger.error(`[SePay Webhook] No product found for transaction: ${orderCode}`) + throw createError({ + statusCode: 400, + message: 'No product found for this transaction!', + }) + } + + logger.log(`[SePay Webhook] Updating transaction ${paymentTransactionOfProvider.id} to status: ${transactionStatus}`) + + await updateProviderTransactionStatus(paymentTransactionOfProvider.id, transactionStatus, body.transactionDate!) + + await updatePaymentStatus(paymentTransactionOfProvider.payment.id, transactionStatus) + + if (reference) { + await createReferenceUsage( + userId, + reference.id || '', + paymentTransactionOfProvider.id, + ) + } + + logger.log(`[SePay Webhook] Transaction updated successfully: id=${paymentTransactionOfProvider.id}, status=${transactionStatus}`) + + logger.log('[SePay Webhook] Webhook processing completed successfully') + return { success: true } + } + catch (error: any) { + logger.error('[SePay Webhook] Error processing webhook:', error) + + throw parseError(error) + } +}) diff --git a/server/api/payments/stripe/customers/[customerId]/subscriptions/index.post.ts b/server/api/payments/stripe/customers/[customerId]/subscriptions/index.post.ts index 81916752..6750dbf8 100644 --- a/server/api/payments/stripe/customers/[customerId]/subscriptions/index.post.ts +++ b/server/api/payments/stripe/customers/[customerId]/subscriptions/index.post.ts @@ -1,3 +1,5 @@ +import { useNitroApp } from 'nitropack/runtime' + export default defineEventHandler(async (event) => { const nitroApp = useNitroApp() const { customerId } = await defineEventOptions(event, { auth: true, params: ['customerId'] }) diff --git a/server/api/payments/stripe/customers/index.post.ts b/server/api/payments/stripe/customers/index.post.ts index c5ed8c32..ac77fb18 100644 --- a/server/api/payments/stripe/customers/index.post.ts +++ b/server/api/payments/stripe/customers/index.post.ts @@ -1,3 +1,5 @@ +import { useNitroApp } from 'nitropack/runtime' + export default defineEventHandler(async (event) => { const nitroApp = useNitroApp() await defineEventOptions(event, { auth: true }) diff --git a/server/api/payments/stripe/me.get.ts b/server/api/payments/stripe/me.get.ts index ac919cb1..339cfa7f 100644 --- a/server/api/payments/stripe/me.get.ts +++ b/server/api/payments/stripe/me.get.ts @@ -6,10 +6,10 @@ export default defineEventHandler(async (event) => { let subscriptions: Stripe.Subscription[] = [] - let customer = await getStripeCustomerByEmail(session.email!) + let customer = await getStripeCustomerByEmail(session.email as string) if (!customer) { - const { subscription, customer: newCustomer } = await createStripeCustomerOnSignup(session.email!) + const { subscription, customer: newCustomer } = await createStripeCustomerOnSignup(session.email as string) customer = newCustomer diff --git a/server/api/payments/stripe/products/[productId]/prices/index.get.ts b/server/api/payments/stripe/products/[productId]/prices/index.get.ts index 92506eb3..810d5b9b 100644 --- a/server/api/payments/stripe/products/[productId]/prices/index.get.ts +++ b/server/api/payments/stripe/products/[productId]/prices/index.get.ts @@ -1,7 +1,7 @@ export default defineEventHandler(async (event) => { - const { productId } = await defineEventOptions(event, { auth: true, params: ['productId'] }) + const { productId } = await defineEventOptions(event, { auth: false, params: ['productId'] }) const prices = await getStripeAllPrices(productId) - return prices + return { data: prices } }) diff --git a/server/api/payments/stripe/products/index.get.ts b/server/api/payments/stripe/products/index.get.ts index a50c3c62..27856f94 100644 --- a/server/api/payments/stripe/products/index.get.ts +++ b/server/api/payments/stripe/products/index.get.ts @@ -3,5 +3,5 @@ export default defineEventHandler(async (event) => { const products = await getStripeAllProducts() - return products + return { data: products } }) diff --git a/server/api/payments/vnpay/IPN.get.ts b/server/api/payments/vnpay/IPN.get.ts deleted file mode 100644 index 0a025287..00000000 --- a/server/api/payments/vnpay/IPN.get.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { eq } from 'drizzle-orm' -import type { VerifyIpnCall } from 'vnpay' -import { - InpOrderAlreadyConfirmed, - IpnFailChecksum, - IpnInvalidAmount, - IpnOrderNotFound, - IpnSuccess, - IpnUnknownError, -} from 'vnpay' -import { PaymentStatus, paymentProviderTransactionTable, userPaymentTable } from '@base/server/db/schemas' - -function convertToSQLDateWithTimezone(input: string): Date { - // Extract date and time components from the input (assuming GMT+7) - const year = Number.parseInt(input.substring(0, 4)) - const month = Number.parseInt(input.substring(4, 6)) - 1 // JavaScript months are 0-indexed - const day = Number.parseInt(input.substring(6, 8)) - const hours = Number.parseInt(input.substring(8, 10)) - const minutes = Number.parseInt(input.substring(10, 12)) - const seconds = Number.parseInt(input.substring(12, 14)) - - // Create a Date object in UTC by adjusting for GMT+7 - const dateInGMT7 = new Date(Date.UTC(year, month, day, hours, minutes, seconds)) - const offsetInMilliseconds = -7 * 60 * 60 * 1000 // Convert from GMT+7 to UTC - const dateInUTC = new Date(dateInGMT7.getTime() - offsetInMilliseconds) - - return dateInUTC -} - -export default defineEventHandler(async (event) => { - try { - const { isSuccess, isVerified, vnp_TxnRef, vnp_TransactionNo, vnp_Amount, vnp_PayDate }: VerifyIpnCall = vnpayAdmin.verifyIpnCall(getQuery(event)) - setResponseStatus(event, 200) - - if (!isVerified) - return IpnFailChecksum - - const { user_payments, payment_provider_transactions } = (await db.select() - .from(userPaymentTable) - .where(eq(userPaymentTable.id, vnp_TxnRef)) - .innerJoin(paymentProviderTransactionTable, eq(paymentProviderTransactionTable.payment_id, userPaymentTable.id)))[0] - - if (!user_payments) - return IpnOrderNotFound - - if (user_payments.amount !== vnp_Amount.toString()) - return IpnInvalidAmount - - if (payment_provider_transactions.provider_transaction_status !== PaymentStatus.PENDING) - return InpOrderAlreadyConfirmed - - const transactionStatus = isSuccess ? PaymentStatus.RESOLVED : PaymentStatus.FAILED - const transactionDate = convertToSQLDateWithTimezone(vnp_PayDate?.toString() || '') - - await db.transaction(async (db) => { - await db.update(paymentProviderTransactionTable).set({ - provider_transaction_id: vnp_TransactionNo?.toString(), - provider_transaction_status: transactionStatus, - provider_transaction_resolved_at: transactionDate, - }).where(eq(paymentProviderTransactionTable.id, payment_provider_transactions.id)) - - await db.update(userPaymentTable).set({ - status: transactionStatus, - }).where(eq(userPaymentTable.id, user_payments.id)) - }) - - return IpnSuccess - } - catch { - return IpnUnknownError - } -}) diff --git a/server/api/payments/vnpay/callback.get.ts b/server/api/payments/vnpay/callback.get.ts deleted file mode 100644 index b096506f..00000000 --- a/server/api/payments/vnpay/callback.get.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { stringify } from 'node:querystring' -import { eq } from 'drizzle-orm' -import type { VerifyIpnCall } from 'vnpay' -import { - InpOrderAlreadyConfirmed, - IpnFailChecksum, - IpnInvalidAmount, - IpnOrderNotFound, - IpnUnknownError, -} from 'vnpay' -import { PaymentStatus, paymentProviderTransactionTable, userPaymentTable } from '@base/server/db/schemas' -import { joinURL, withQuery } from 'ufo' - -function convertToSQLDateWithTimezone(input: string): Date { - // Extract date and time components from the input (assuming GMT+7) - const year = Number.parseInt(input.substring(0, 4)) - const month = Number.parseInt(input.substring(4, 6)) - 1 // JavaScript months are 0-indexed - const day = Number.parseInt(input.substring(6, 8)) - const hours = Number.parseInt(input.substring(8, 10)) - const minutes = Number.parseInt(input.substring(10, 12)) - const seconds = Number.parseInt(input.substring(12, 14)) - - // Create a Date object in UTC by adjusting for GMT+7 - const dateInGMT7 = new Date(Date.UTC(year, month, day, hours, minutes, seconds)) - const offsetInMilliseconds = 7 * 60 * 60 * 1000 // Convert from GMT+7 to UTC - const dateInUTC = new Date(dateInGMT7.getTime() - offsetInMilliseconds) - - return dateInUTC -} - -export default defineEventHandler(async (event) => { - try { - const { session } = await defineEventOptions(event, { - auth: true, - }) - - const { isSuccess, isVerified, vnp_OrderInfo, vnp_TxnRef, vnp_TransactionNo, vnp_Amount, vnp_PayDate }: VerifyIpnCall = vnpayAdmin.verifyIpnCall(getQuery(event)) - - if (!isVerified) - throw new Error('IpnFailChecksum') - - const { user_payments, payment_provider_transactions } = (await db.select() - .from(userPaymentTable) - .where(eq(userPaymentTable.id, vnp_TxnRef)) - .innerJoin(paymentProviderTransactionTable, eq(paymentProviderTransactionTable.payment_id, userPaymentTable.id)))[0] - - if (!user_payments) - throw new Error('IpnOrderNotFound') - - if (user_payments.amount !== vnp_Amount.toString()) - throw new Error('IpnInvalidAmount') - - if (payment_provider_transactions.provider_transaction_status !== PaymentStatus.PENDING) - throw new Error('InpOrderAlreadyConfirmed') - - const transactionStatus = isSuccess ? PaymentStatus.RESOLVED : PaymentStatus.FAILED - const transactionDate = convertToSQLDateWithTimezone(vnp_PayDate?.toString() || '') - - await db.transaction(async (db) => { - await db.update(paymentProviderTransactionTable).set({ - provider_transaction_id: vnp_TransactionNo?.toString(), - provider_transaction_status: transactionStatus, - provider_transaction_resolved_at: transactionDate, - }).where(eq(paymentProviderTransactionTable.id, payment_provider_transactions.id)) - - await db.update(userPaymentTable).set({ - status: transactionStatus, - }).where(eq(userPaymentTable.id, user_payments.id)) - }) - - // CHECK AND HANDLE TOP UP PAYMENT - if (isSuccess && vnp_OrderInfo.includes('credit')) { - const [_, amount] = payment_provider_transactions.provider_transaction_info.split(':') - await addCreditToUser(session, Number.parseInt(amount)) - } - - const runtimeConfig = useRuntimeConfig() - // TODO: Do something with the success - return sendRedirect( - event, - withQuery( - joinURL( - runtimeConfig.public.appBaseUrl, - runtimeConfig.public.appPaymentRedirect, - ), - { paymentStatus: isSuccess ? 'success' : 'fail' }, - ), - 200, - ) - } - catch (error: any) { - const runtimeConfig = useRuntimeConfig() - let errResponse = IpnUnknownError - switch (error.message) { - case 'IpnFailChecksum': - errResponse = IpnFailChecksum - break - case 'IpnOrderNotFound': - errResponse = IpnOrderNotFound - break - case 'IpnInvalidAmount': - errResponse = IpnInvalidAmount - break - case 'InpOrderAlreadyConfirmed': - errResponse = InpOrderAlreadyConfirmed - break - default: - break - } - const queryString = stringify(errResponse) - // TODO: Do something with the error - return sendRedirect( - event, - `${joinURL( - runtimeConfig.public.appBaseUrl, - runtimeConfig.public.appPaymentRedirect, - )}?${queryString}`, - 200, - ) - } -}) diff --git a/server/api/payments/vnpay/query.post.ts b/server/api/payments/vnpay/query.post.ts deleted file mode 100644 index c5ff7b40..00000000 --- a/server/api/payments/vnpay/query.post.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { QueryDr, QueryDrResponse } from 'vnpay' -import { generateRandomString } from 'vnpay' -import { format } from 'date-fns' -import { paymentProviderTransactionTable, userPaymentTable } from '@base/server/db/schemas' -import { eq } from 'drizzle-orm' - -export default defineEventHandler(async (event) => { - try { - const { paymentId } = await readBody(event) - - const { user_payments, payment_provider_transactions } = (await db.select() - .from(userPaymentTable) - .where(eq(userPaymentTable.id, paymentId)) - .innerJoin(paymentProviderTransactionTable, eq(paymentProviderTransactionTable.payment_id, userPaymentTable.id)))[0] - - if (!user_payments || !payment_provider_transactions) { - throw new Error('Payment not found') - } - - const ipAddr = getRequestIP(event) - const date = new Date().getTime().toString() - const data: QueryDrResponse = await vnpayAdmin.queryDr({ - vnp_RequestId: generateRandomString(16), - vnp_IpAddr: ipAddr || '127.0.0.1', - vnp_TxnRef: paymentId, - vnp_OrderInfo: payment_provider_transactions.provider_transaction_info, - vnp_TransactionNo: payment_provider_transactions.provider_transaction_id, - vnp_TransactionDate: Number(format(payment_provider_transactions.provider_transaction_resolved_at ?? new Date(), 'yyyyMMddHHmmss')), - vnp_CreateDate: date, - } as unknown as QueryDr) - - setResponseStatus(event, 200) - return { - message: 'Success', - data, - } - } - catch (error: any) { - throw parseError(error) - } -}) diff --git a/server/api/products/credit-packages.get.ts b/server/api/products/credit-packages.get.ts new file mode 100644 index 00000000..89ad23c1 --- /dev/null +++ b/server/api/products/credit-packages.get.ts @@ -0,0 +1,16 @@ +export default defineEventHandler(async (event) => { + try { + await defineEventOptions(event) + + const { getCreditPackages } = useProduct() + + const creditPackages = await getCreditPackages() + + return { data: creditPackages } + } + catch (error: any) { + logger.error('[Products API] Error fetching products:', error) + + throw parseError(error) + } +}) diff --git a/server/api/products/index.get.ts b/server/api/products/index.get.ts new file mode 100644 index 00000000..4b7c3f2e --- /dev/null +++ b/server/api/products/index.get.ts @@ -0,0 +1,16 @@ +export default defineEventHandler(async (event) => { + try { + await defineEventOptions(event) + + const { getProducts } = useProduct() + + const products = await getProducts() + + return { data: products } + } + catch (error: any) { + logger.error('[Products API] Error fetching products:', error) + + throw parseError(error) + } +}) diff --git a/server/api/ref/[referCode]/apply.get.ts b/server/api/ref/[referCode]/apply.get.ts new file mode 100644 index 00000000..e993ac1a --- /dev/null +++ b/server/api/ref/[referCode]/apply.get.ts @@ -0,0 +1,24 @@ +import { withQuery } from 'ufo' + +export default defineEventHandler(async (event) => { + try { + const { referCode } = await defineEventOptions(event, { params: [REFERENCE_CODE_COOKIE_NAME] }) + const { session } = await defineEventOptions(event, { auth: true }) + + const { isReferenceUsableByUser } = useReference() + const isUseable = await isReferenceUsableByUser(referCode, session.id) + if(isUseable) { + setCookie(event, REFERENCE_CODE_COOKIE_NAME, referCode, { + httpOnly: false + }) + } + + return sendRedirect(event, withQuery('/pricing', { referCode }), 301) + } + catch (error: any) { + throw createError({ + statusCode: 503, + statusMessage: error.message, + }) + } +}) diff --git a/server/api/ref/[referCode]/index.get.ts b/server/api/ref/[referCode]/index.get.ts new file mode 100644 index 00000000..0e33ffbe --- /dev/null +++ b/server/api/ref/[referCode]/index.get.ts @@ -0,0 +1,15 @@ +import { withQuery } from 'ufo' + +export default defineEventHandler(async (event) => { + try { + const { referCode } = await defineEventOptions(event, { auth: true, params: ['referCode'] }) + const { getReferenceByCode } = useReference() + + return getReferenceByCode(referCode) + } + catch (error: any) { + logger.error('[Reference API] Error fetching reference by refCode:', error) + + throw parseError(error) + } +}) diff --git a/server/api/ref/available.get.ts b/server/api/ref/available.get.ts new file mode 100644 index 00000000..78c4da22 --- /dev/null +++ b/server/api/ref/available.get.ts @@ -0,0 +1,14 @@ +export default defineEventHandler(async (event) => { + try { + const { session } = await defineEventOptions(event, { auth: true }) + + const { getAvailableReferencesByUserId } = useReference() + + return getAvailableReferencesByUserId(session.id) + } + catch (error: any) { + logger.error('[Reference API] Error fetching available references:', error) + + throw parseError(error) + } +}) diff --git a/server/api/ref/history.get.ts b/server/api/ref/history.get.ts new file mode 100644 index 00000000..3b71e551 --- /dev/null +++ b/server/api/ref/history.get.ts @@ -0,0 +1,14 @@ +export default defineEventHandler(async (event) => { + try { + const { session } = await defineEventOptions(event, { auth: true }) + + const { getReferenceUsageHistoryByUser } = useReference() + + return getReferenceUsageHistoryByUser(session.id, getFilter(event)) + } + catch (error: any) { + logger.error('[Reference API] Error fetching usage history references:', error) + + throw parseError(error) + } +}) diff --git a/server/api/scopes.get.ts b/server/api/scopes.get.ts index e4273dfc..815535f7 100644 --- a/server/api/scopes.get.ts +++ b/server/api/scopes.get.ts @@ -5,6 +5,8 @@ export default defineEventHandler(async (event) => { return await getUserScopes() } catch (error: any) { + logger.error('[Scopes API] Error fetching scopes:', error) + throw parseError(error) } }) diff --git a/server/api/users/[userId]/devices/index.delete.ts b/server/api/users/[userId]/devices/index.delete.ts deleted file mode 100644 index 6ae68cec..00000000 --- a/server/api/users/[userId]/devices/index.delete.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useUserDeviceCrud } from '@base/server/composables/useUserDeviceCrud' - -export default defineEventHandler(async (event) => { - try { - const { userId } = await defineEventOptions(event, { auth: true, params: ['userId'] }) - const { token } = await readBody(event) - - const { deleteUserDeviceToken } = useUserDeviceCrud({ user_id: userId }) - await deleteUserDeviceToken(token) - setResponseStatus(event, 200) - return { message: 'Token unregistration successful' } - } - catch (error: any) { - throw parseError(error) - } -}) diff --git a/server/api/users/[userId]/devices/index.get.ts b/server/api/users/[userId]/devices/index.get.ts deleted file mode 100644 index 8d35b188..00000000 --- a/server/api/users/[userId]/devices/index.get.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useUserDeviceCrud } from '@base/server/composables/useUserDeviceCrud' - -export default defineEventHandler(async (event) => { - try { - const { userId } = await defineEventOptions(event, { auth: true, params: ['userId'] }) - - const { getUserDeviceAllTokens } = useUserDeviceCrud({ user_id: userId }) - - const tokens = await getUserDeviceAllTokens({} as ParsedFilterQuery) - setResponseStatus(event, 200) - return tokens - } - catch (error: any) { - throw parseError(error) - } -}) diff --git a/server/api/users/[userId]/devices/index.post.ts b/server/api/users/[userId]/devices/index.post.ts deleted file mode 100644 index 83cd65e0..00000000 --- a/server/api/users/[userId]/devices/index.post.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useUserDeviceCrud } from '@base/server/composables/useUserDeviceCrud' -import { z } from 'zod' - -export default defineEventHandler(async (event) => { - const { session, userId } = await defineEventOptions(event, { auth: true, params: ['userId'] }) - - const nitroApp = useNitroApp() - - try { - const { token } = await readValidatedBody(event, z.object({ token: z.string() }).parse) - - const { getUserDeviceToken, createUserDeviceToken } = useUserDeviceCrud({ user_id: userId }) - const dataTokenExists = await getUserDeviceToken(token) - - if (!dataTokenExists?.data) { - const tokenRegistered = await createUserDeviceToken({ - user_id: userId, - token_device: token, - }) - return { message: 'Token registration successful', token: tokenRegistered.data.token_device } - } - return { message: 'Token registration successful' } - } - catch (error: any) { - const _error = parseError(error) - - if (_error.data?.code === '23503') - await nitroApp.hooks.callHook('session:cache:clear', { providerAccountId: session.sub }) - - throw _error - } -}) diff --git a/server/api/users/[userId]/notifications/[notificationUId].delete.ts b/server/api/users/[userId]/notifications/[notificationUId].delete.ts deleted file mode 100644 index 7d00f8d9..00000000 --- a/server/api/users/[userId]/notifications/[notificationUId].delete.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useNotificationCrud } from '@base/server/composables/useNotificationCrud' - -export default defineEventHandler(async (event) => { - try { - const { userId, notificationUId } = await defineEventOptions(event, { auth: true, params: ['userId', 'notificationUId'] }) - - const queryRestrict = { user_id: userId } - const { deleteNotificationById } = useNotificationCrud(queryRestrict) - - const data = await deleteNotificationById(notificationUId) - - setResponseStatus(event, 200) - - return data - } - catch (error: any) { - throw parseError(error) - } -}) diff --git a/server/api/users/[userId]/notifications/[notificationUId].patch.ts b/server/api/users/[userId]/notifications/[notificationUId].patch.ts deleted file mode 100644 index 4fd31caa..00000000 --- a/server/api/users/[userId]/notifications/[notificationUId].patch.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useNotificationCrud } from '@base/server/composables/useNotificationCrud' -import { createInsertSchema } from 'drizzle-zod' -import { sysNotificationTable } from '@base/server/db/schemas' - -export default defineEventHandler(async (event) => { - try { - const { userId, notificationUId } = await defineEventOptions(event, { auth: true, params: ['userId', 'notificationUId'] }) - - const queryRestrict = { user_id: userId } - const { updateNotificationById } = useNotificationCrud(queryRestrict) - - const body = await readValidatedBody(event, createInsertSchema(sysNotificationTable).partial().parse) - - if (body && body.read_at) { - body.read_at = new Date(body.read_at) - } - - const data = await updateNotificationById(notificationUId, body) - - setResponseStatus(event, 200) - - return data - } - catch (error: any) { - throw parseError(error) - } -}) diff --git a/server/api/users/[userId]/notifications/index.get.ts b/server/api/users/[userId]/notifications/index.get.ts deleted file mode 100644 index fff7fe9d..00000000 --- a/server/api/users/[userId]/notifications/index.get.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useNotificationCrud } from '@base/server/composables/useNotificationCrud' - -export default defineEventHandler(async (event) => { - try { - const { userId } = await defineEventOptions(event, { auth: true, params: ['userId'] }) - - const queryRestrict = { user_id: userId } - const { getNotificationsPaginated } = useNotificationCrud(queryRestrict) - - const notifications = await getNotificationsPaginated(getFilter(event)) - - setResponseStatus(event, 200) - - return notifications.data - } - catch (error: any) { - throw parseError(error) - } -}) diff --git a/server/api/users/[userId]/notifications/mark-all-read.patch.ts b/server/api/users/[userId]/notifications/mark-all-read.patch.ts deleted file mode 100644 index 12fbe022..00000000 --- a/server/api/users/[userId]/notifications/mark-all-read.patch.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useNotificationCrud } from '@base/server/composables/useNotificationCrud' - -export default defineEventHandler(async (event) => { - try { - const { userId } = await defineEventOptions(event, { auth: true, params: ['userId'] }) - - const queryRestrict = { user_id: userId, markAllRead: true } - const { markAllRead } = useNotificationCrud(queryRestrict) - const response = await markAllRead() - - return response - } - catch (error: any) { - throw parseError(error) - } -}) diff --git a/server/api/users/[userId]/notifications/mark-all-unread.patch.ts b/server/api/users/[userId]/notifications/mark-all-unread.patch.ts deleted file mode 100644 index 23923744..00000000 --- a/server/api/users/[userId]/notifications/mark-all-unread.patch.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useNotificationCrud } from '@base/server/composables/useNotificationCrud' - -export default defineEventHandler(async (event) => { - try { - const { userId } = await defineEventOptions(event, { auth: true, params: ['userId'] }) - - const queryRestrict = { user_id: userId, markAllUnread: true } - const { markAllUnread } = useNotificationCrud(queryRestrict) - const response = await markAllUnread() - - return response - } - catch (error: any) { - throw parseError(error) - } -}) diff --git a/server/api/users/[userId]/notifications/unread.get.ts b/server/api/users/[userId]/notifications/unread.get.ts deleted file mode 100644 index aaaf0c3a..00000000 --- a/server/api/users/[userId]/notifications/unread.get.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { useNotificationCrud } from '@base/server/composables/useNotificationCrud' - -export default defineEventHandler(async (event) => { - try { - const { userId } = await defineEventOptions(event, { auth: true, params: ['userId'] }) - const queryRestrict = { user_id: userId, markAllRead: true } - const { countNotifications } = useNotificationCrud(queryRestrict) - const notifications = await countNotifications() - setResponseStatus(event, 200) - return notifications - } - catch (error: any) { - throw parseError(error) - } -}) diff --git a/server/api/users/[userId]/shortcuts/[shortcutUId].delete.ts b/server/api/users/[userId]/shortcuts/[shortcutUId].delete.ts deleted file mode 100644 index fb0be76d..00000000 --- a/server/api/users/[userId]/shortcuts/[shortcutUId].delete.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useShortcutCrud } from '@base/server/composables/useShortcutCrud' - -export default defineEventHandler(async (event) => { - try { - const { userId, shortcutUId } = await defineEventOptions(event, { auth: true, params: ['userId', 'shortcutUId'] }) - - const { deleteShortcutById } = useShortcutCrud(userId) - - const data = await deleteShortcutById(shortcutUId) - - setResponseStatus(event, 200) - - return data - } - catch (error: any) { - throw parseError(error) - } -}) diff --git a/server/api/users/[userId]/shortcuts/[shortcutUId].get.ts b/server/api/users/[userId]/shortcuts/[shortcutUId].get.ts deleted file mode 100644 index adabda33..00000000 --- a/server/api/users/[userId]/shortcuts/[shortcutUId].get.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { and, eq } from 'drizzle-orm' -import { userShortcutTable } from '@base/server/db/schemas' - -export default defineEventHandler(async (event) => { - try { - const { userId, shortcutUId } = await defineEventOptions(event, { auth: true, params: ['userId', 'shortcutUId'] }) - - const userShortcut = await db.select().from(userShortcutTable) - .where( - and( - eq(userShortcutTable.user_id, userId), - eq(userShortcutTable.id, shortcutUId), - ), - ) - .limit(1) - - setResponseStatus(event, 201) - - return { data: userShortcut[0] } - } - catch (error: any) { - throw parseError(error) - } -}) diff --git a/server/api/users/[userId]/shortcuts/[shortcutUId].patch.ts b/server/api/users/[userId]/shortcuts/[shortcutUId].patch.ts deleted file mode 100644 index 357ad205..00000000 --- a/server/api/users/[userId]/shortcuts/[shortcutUId].patch.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { eq } from 'drizzle-orm' -import { userShortcutTable } from '@base/server/db/schemas' -import { createInsertSchema } from 'drizzle-zod' - -export default defineEventHandler(async (event) => { - try { - const { userId, shortcutUId } = await defineEventOptions(event, { auth: true, params: ['userId', 'shortcutUId'] }) - - const body = await readValidatedBody(event, createInsertSchema(userShortcutTable).partial().parse) - - const userShortcut = await db.update(userShortcutTable) - .set({ ...body, user_id: userId }) - .where(eq(userShortcutTable.id, shortcutUId)) - .returning() - - setResponseStatus(event, 201) - - return { data: userShortcut } - } - catch (error: any) { - throw parseError(error) - } -}) diff --git a/server/api/users/[userId]/shortcuts/count.get.ts b/server/api/users/[userId]/shortcuts/count.get.ts deleted file mode 100644 index 8e89d810..00000000 --- a/server/api/users/[userId]/shortcuts/count.get.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { count, eq } from 'drizzle-orm' -import { userShortcutTable } from '@base/server/db/schemas' - -export default defineEventHandler(async (event) => { - try { - const { userId } = await defineEventOptions(event, { auth: true, params: ['userId'] }) - - const userShortcutSubquery = db.select().from(userShortcutTable) - .where( - eq(userShortcutTable.user_id, userId), - ) - - const total = await db.select({ count: count() }).from(userShortcutSubquery.as('count')) - - return { - total, - } - } - catch (error: any) { - throw parseError(error) - } -}) diff --git a/server/api/users/[userId]/shortcuts/index.get.ts b/server/api/users/[userId]/shortcuts/index.get.ts deleted file mode 100644 index de58326b..00000000 --- a/server/api/users/[userId]/shortcuts/index.get.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useShortcutCrud } from '@base/server/composables/useShortcutCrud' - -export default defineEventHandler(async (event) => { - try { - const { userId } = await defineEventOptions(event, { auth: true, params: ['userId'] }) - - const { getShortcutsPaginated } = useShortcutCrud(userId) - - const userShortcuts = await getShortcutsPaginated({ - ...getFilter(event), - sortBy: 'route', - }) - - return userShortcuts - } - catch (error: any) { - throw parseError(error) - } -}) diff --git a/server/api/users/[userId]/shortcuts/index.post.ts b/server/api/users/[userId]/shortcuts/index.post.ts deleted file mode 100644 index d5d61297..00000000 --- a/server/api/users/[userId]/shortcuts/index.post.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useShortcutCrud } from '@base/server/composables/useShortcutCrud' -import { createInsertSchema } from 'drizzle-zod' -import { userShortcutTable } from '@base/server/db/schemas' - -export default defineEventHandler(async (event) => { - try { - const { userId } = await defineEventOptions(event, { auth: true, params: ['userId'] }) - - const body = await readValidatedBody(event, createInsertSchema(userShortcutTable).partial().parse) - - const { createShortcut } = useShortcutCrud(userId) - - const userShortcut = await createShortcut({ ...body, user_id: userId }) - - setResponseStatus(event, 201) - - return userShortcut - } - catch (error: any) { - throw parseError(error) - } -}) diff --git a/server/composables/useCredit.ts b/server/composables/useCredit.ts new file mode 100644 index 00000000..beb0b6b6 --- /dev/null +++ b/server/composables/useCredit.ts @@ -0,0 +1,41 @@ +import type { CreditHistoryType } from '@base/server/db/schemas' +import { ProductStatus, creditHistoryTable } from '@base/server/db/schemas' +import type { CreditHistory, Product } from '@base/server/types/models' + +export function useCredit() { + function getProducts(): Promise { + return db.query.productTable.findMany({ + where(schema, { eq }) { + return eq(schema.status, ProductStatus.ACTIVE) + }, + orderBy(fields, { asc }) { + return asc(fields.price) + }, + }) + } + + async function updateCreditHistory( + type: CreditHistoryType, + amount: number, + userId: string, + ): Promise { + return (await db.insert(creditHistoryTable) + .values({ + amount, + type, + user_id: userId, + }) + .returning())[0] + } + + function updateUserCredit(userId: string, credit: number) { + return useUser() + .upsertUser(userId, { credit }) + } + + return { + getProducts, + updateCreditHistory, + updateUserCredit, + } +} diff --git a/server/composables/useCrud.ts b/server/composables/useCrud.ts deleted file mode 100644 index eaea4fc9..00000000 --- a/server/composables/useCrud.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { and, asc, count, desc, eq, ilike, or } from 'drizzle-orm' -import type { InferInsertModel, InferSelectModel } from 'drizzle-orm' -import type { PgTable } from 'drizzle-orm/pg-core' -import type { ParsedFilterQuery } from '@base/server/utils/filter' - -interface CrudOptions { - searchBy?: Array - queryRestrict?: () => any -} - -export function useCrud(sourceTable: T, options?: CrudOptions) { - async function getRecordsPaginated(opts: Partial) { - const { keyword = '', keywordLower = '', sortBy = 'created_at', sortAsc = true, limit = 10, page = 1, withCount = false } = opts - - const searchConditions = [] - - if (Array.isArray(options?.searchBy)) { - for (const field of options.searchBy) { - if (keyword || keywordLower) { - searchConditions.push(...[ - sourceTable[field] && ilike(sourceTable[field] as any, `%${keyword || ''}%`), - sourceTable[field] && ilike(sourceTable[field] as any, `%${keywordLower || ''}%`), - ]) - } - } - } - - const sysRecordSubquery = db.select().from(sourceTable) - .where( - and(...[ - options?.queryRestrict?.(), - searchConditions.length && or( - ...searchConditions.filter(Boolean), - ), - ].filter(Boolean)), - ) - - let total = 0 - - if (withCount) { - total = ( - await db.select({ count: count() }).from(sysRecordSubquery.as('count')) - )[0]?.count || 0 - } - - const sysRecords = await sysRecordSubquery - .orderBy( - sortAsc ? asc((sourceTable as any)[sortBy]) : desc((sourceTable as any)[sortBy]), - ) - .offset((page - 1) * limit) - .limit(limit) - - if (!withCount) - total = sysRecords.length - - return { - data: sysRecords as InferSelectModel[], - total, - } - } - - async function getRecordByKey(key: keyof T, value: any) { - const sysRecord = ( - await db.select().from(sourceTable) - .where( - and( - ...[ - options?.queryRestrict?.(), - eq(sourceTable[key] as any, value), - ].filter(Boolean), - - ), - ) - .limit(1) - )[0] - - return { data: sysRecord as InferSelectModel } - } - - async function updateRecordByKey(key: keyof T, value: any, body: InferInsertModel) { - const sysRecord = ( - await db.update(sourceTable) - .set(body) - .where( - and( - - ...[ - options?.queryRestrict?.(), - eq(sourceTable[key] as any, value), - ].filter(Boolean), - ), - ) - .returning() - )[0] - - return { data: sysRecord as InferSelectModel } - } - - async function createRecord(body: InferInsertModel) { - const sysRecord = ( - await db.insert(sourceTable) - .values(body) - .returning() - )[0] - - return { data: sysRecord as InferSelectModel } - } - - async function deleteRecordByKey(key: keyof T, value: any) { - const sysRecord = await db.delete(sourceTable) - .where( - and( - ...[ - options?.queryRestrict?.(), - eq(sourceTable[key] as any, value), - ].filter(Boolean), - ), - ) - .returning() - - return { data: sysRecord as InferSelectModel } - } - - async function countRecords() { - const sysRecordSubquery = db.select().from(sourceTable).where(options?.queryRestrict?.()) - - const response = ( - await db.select({ count: count() }).from(sysRecordSubquery.as('count')) - )[0] - - return { - total: response?.count || 0, - } - } - async function updateManyRecords(body: InferInsertModel) { - if (!options?.queryRestrict) { - throw createError({ - statusCode: 500, - statusMessage: 'Query restrict option is required for updating many records.', - }) - } - - const sysRecord = ( - await db.update(sourceTable) - .set(body) - .where( - options?.queryRestrict?.(), - ) - .returning() - ) - - return { data: sysRecord as InferSelectModel[] } - } - - return { - getRecordsPaginated, - getRecordByKey, - createRecord, - updateRecordByKey, - deleteRecordByKey, - countRecords, - updateManyRecords, - } -} diff --git a/server/composables/useDeviceToken.ts b/server/composables/useDeviceToken.ts new file mode 100644 index 00000000..83a9423e --- /dev/null +++ b/server/composables/useDeviceToken.ts @@ -0,0 +1,42 @@ +import { and, eq } from 'drizzle-orm' +import { deviceTable } from '@base/server/db/schemas' +import type { Device } from '@base/server/types/models' + +export function useDeviceToken() { + async function getDeviceTokens(userId: string): Promise { + return db.query.deviceTable.findMany({ + where: eq(deviceTable.user_id, userId), + }) + } + + function getDeviceToken(userId: string, deviceToken: string): Promise { + return db.query.deviceTable.findFirst({ + where: (schema, { eq }) => { + return eq(schema.token_device, deviceToken) + }, + }) + } + + async function createDeviceToken(userId: string, deviceToken: string): Promise { + return (await db.insert(deviceTable).values({ + user_id: userId, + token_device: deviceToken, + }).returning())[0] + } + + function deleteDeviceToken(userId: string, deviceToken: string) { + return db.delete(deviceTable).where( + and( + eq(deviceTable.user_id, userId), + eq(deviceTable.token_device, deviceToken), + ), + ) + } + + return { + getDeviceToken, + getDeviceTokens, + createDeviceToken, + deleteDeviceToken, + } +} diff --git a/server/composables/useFaqCrud.ts b/server/composables/useFaqCrud.ts deleted file mode 100644 index a89cd944..00000000 --- a/server/composables/useFaqCrud.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { sysFaqTable } from '../db/schemas/sys_faqs.schema' -import { sysFaqCategoryTable } from '../db/schemas/sys_faq_categories.schema' -import { useCrud } from './useCrud' - -export function useFaqCrud() { - const { getRecordsPaginated: getRecordsFaqCategories } = useCrud(sysFaqCategoryTable) - - const { getRecordsPaginated } = useCrud(sysFaqTable, { - searchBy: ['question'], - }) - - async function getFaqQuestions(options: ParsedFilterQuery) { - const { data: categories } = await getRecordsFaqCategories({}) - - const { data: faqs } = await getRecordsPaginated(options) - - const faqCategories = categories.map(category => ({ ...category, questions: [] as typeof faqs })) - - for (const faq of faqs) { - const category = faqCategories.find(item => item.id === faq.category_id) - if (category) { - category.questions.push(faq) - } - } - return faqCategories - } - - return { - getFaqQuestions, - } -} diff --git a/server/composables/useIdentity.ts b/server/composables/useIdentity.ts new file mode 100644 index 00000000..9bffcbaf --- /dev/null +++ b/server/composables/useIdentity.ts @@ -0,0 +1,79 @@ +import { and, eq } from 'drizzle-orm' +import { identityTable } from '../db/schemas' +import type { Identity } from '../types/models' + +export function useIdentity() { + function getIdentitiesByUserId(userId: string): Promise { + return db.query.identityTable.findMany({ + where: eq(identityTable.user_id, userId), + }) + } + + function getIdentityByProvider(userId: string, provider: string): Promise { + return db.query.identityTable.findFirst({ + where: (fields, { and, eq }) => and( + eq(fields.user_id, userId), + eq(fields.provider, provider), + ), + }) + } + + async function createIdentity( + userId: string, + provider: string, + providerUserId: string, + providerData: Record, + ): Promise { + return (await db.insert(identityTable).values({ + user_id: userId, + provider, + provider_user_id: providerUserId, + provider_data: providerData || {}, + }).returning())[0] + } + + async function updateIdentity( + identityId: string, + data: Partial>, + ): Promise { + return (await db.update(identityTable) + .set({ + ...data, + updated_at: new Date(), + }) + .where(eq(identityTable.id, identityId)) + .returning())[0] + } + + async function upsertIdentity( + userId: string, + provider: string, + providerUserId: string, + providerData: Record, + ): Promise { + const existingIdentity = await getIdentityByProvider(userId, provider) + + if (existingIdentity) { + return updateIdentity(existingIdentity.id, { + provider_user_id: providerUserId, + provider_data: providerData, + }) + } + + return createIdentity(userId, provider, providerUserId, providerData) + } + + function deleteIdentitiesByUserId(userId: string) { + return db.delete(identityTable) + .where(eq(identityTable.user_id, userId)) + } + + return { + getIdentitiesByUserId, + getIdentityByProvider, + createIdentity, + updateIdentity, + upsertIdentity, + deleteIdentitiesByUserId, + } +} diff --git a/server/composables/useNotification.ts b/server/composables/useNotification.ts new file mode 100644 index 00000000..7f2f1768 --- /dev/null +++ b/server/composables/useNotification.ts @@ -0,0 +1,173 @@ +import { and, count, eq, ilike, isNotNull, isNull, or } from 'drizzle-orm' +import { notificationTable } from '@base/server/db/schemas' +import type { Notification, PaginatedResponse, PaginationOptions } from '@base/server/types/models' + +export function useNotification() { + async function getNotificationCount( + userId: string, + options: Partial, + unread?: boolean, + ): Promise<{ total: number }> { + const data = await db.select({ total: count() }).from(notificationTable).where( + and( + eq(notificationTable.user_id, userId), + unread ? isNull(notificationTable.read_at) : undefined, + or( + ilike(notificationTable.title, `%${options?.keyword || ''}%`), + ilike(notificationTable.message, `%${options?.keyword || ''}%`), + ilike(notificationTable.title, `%${options?.keywordLower || ''}%`), + ilike(notificationTable.message, `%${options?.keywordLower || ''}%`), + ), + ), + ) + + return data[0] + } + + async function getNotificationsPaginated( + userId: string, + options: Partial, + unread?: boolean, + ): Promise> { + const limit = options.limit || 20 + const page = options.page || 1 + + const notifications = await db.query.notificationTable.findMany({ + limit, + offset: limit * (page - 1), + orderBy(schema, { asc, desc }) { + return options.sortAsc + ? asc((schema as any)[options.sortBy || 'created_at']) + : desc((schema as any)[options.sortBy || 'created_at']) + }, + where(schema, { and, or, eq, ilike }) { + if (options.keyword && options.keywordLower) { + return and( + eq(schema.user_id, userId), + unread ? isNull(schema.read_at) : undefined, + or( + ilike(schema.title, `%${options.keyword}%`), + ilike(schema.message, `%${options.keyword}%`), + ilike(schema.title, `%${options.keywordLower}%`), + ilike(schema.message, `%${options.keywordLower}%`), + ), + ) + } + }, + }) + + const { total } = options.withCount + ? await getNotificationCount(userId, options, unread) + : { total: notifications.length } + + return { + data: notifications, + total, + } + } + + function getNotificationById(notificationId: string, userId: string): Promise { + return db.query.notificationTable.findFirst({ + where(schema, { and, eq }) { + return and( + eq(schema.user_id, userId), + eq(schema.id, notificationId), + ) + }, + }) + } + + type NotificationInput = Pick + + async function createNotification(userId: string, payload: Omit): Promise { + return (await db.insert(notificationTable).values({ + ...payload, + user_id: userId, + }).returning())[0] + } + + function readNotificationById(notificationId: string, userId: string) { + return db.update(notificationTable).set({ + read_at: new Date(), + }).where( + and( + eq(notificationTable.user_id, userId), + eq(notificationTable.id, notificationId), + ), + ) + } + + function unreadNotificationById(notificationId: string, userId: string) { + return db.update(notificationTable).set({ + read_at: null, + }).where( + and( + eq(notificationTable.user_id, userId), + eq(notificationTable.id, notificationId), + ), + ) + } + + function deleteNotificationById(notificationId: string, userId: string) { + return db.delete(notificationTable).where( + and( + eq(notificationTable.user_id, userId), + eq(notificationTable.id, notificationId), + ), + ) + } + + function markAllRead(userId: string) { + return db.update(notificationTable).set({ + read_at: new Date(), + }).where( + and( + eq(notificationTable.user_id, userId), + isNull(notificationTable.read_at), + ), + ) + } + + function markAllUnread(userId: string) { + return db.update(notificationTable).set({ + read_at: null, + }).where( + and( + eq(notificationTable.user_id, userId), + isNotNull(notificationTable.read_at), + ), + ) + } + + async function getUnreadNotification(userId: string): Promise> { + const notifications = await db.query.notificationTable.findMany({ + where(schema, { and, eq, isNull }) { + return and( + eq(schema.user_id, userId), + isNull(schema.read_at), + ) + }, + orderBy(schema, { desc }) { + return desc(schema.created_at) + }, + }) + + return { + data: notifications, + total: notifications.length, + } + } + + return { + getNotificationsPaginated, + getNotificationCount, + getNotificationById, + createNotification, + readNotificationById, + unreadNotificationById, + deleteNotificationById, + markAllRead, + markAllUnread, + getUnreadNotification + } +} diff --git a/server/composables/useNotificationCrud.ts b/server/composables/useNotificationCrud.ts deleted file mode 100644 index 591fbc9b..00000000 --- a/server/composables/useNotificationCrud.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { and, eq, isNotNull, isNull } from 'drizzle-orm' -import { sysNotificationTable } from '@base/server/db/schemas' -import { useCrud } from '@base/server/composables/useCrud' -import type { ParsedFilterQuery } from '@base/server/utils/filter' - -interface QueryRestrict { - user_id: string - markAllRead?: any - markAllUnread?: any -} -export function useNotificationCrud(queryRestrict: QueryRestrict) { - const { - countRecords, - createRecord, - deleteRecordByKey, - getRecordByKey, - getRecordsPaginated, - updateRecordByKey, - updateManyRecords, - } = useCrud(sysNotificationTable, { - queryRestrict: () => and(...[ - queryRestrict.user_id && eq(sysNotificationTable.user_id, queryRestrict.user_id), - queryRestrict.markAllRead && isNull(sysNotificationTable.read_at), - queryRestrict.markAllUnread && isNotNull(sysNotificationTable.read_at), - ].filter(Boolean)), - }) - async function getNotificationsPaginated(options: ParsedFilterQuery) { - const { data, total } = await getRecordsPaginated(options) - return { data, total } - } - async function getNotificationById(id: string) { - const { data } = await getRecordByKey('id', id) - return { data } - } - async function updateNotificationById(id: string, body: any) { - const { data } = await updateRecordByKey('id', id, body) - return { data } - } - async function createNotification(body: any) { - const { data } = await createRecord(body) - return { data } - } - async function deleteNotificationById(id: string) { - const { data } = await deleteRecordByKey('id', id) - return { data } - } - function countNotifications() { - return countRecords() - } - function markAllRead() { - return updateManyRecords({ read_at: new Date() }) - } - function markAllUnread() { - return updateManyRecords({ read_at: null }) - } - return { - getNotificationsPaginated, - getNotificationById, - createNotification, - updateNotificationById, - deleteNotificationById, - countNotifications, - markAllRead, - markAllUnread, - } -} diff --git a/server/composables/useOrder.ts b/server/composables/useOrder.ts new file mode 100644 index 00000000..f1d94186 --- /dev/null +++ b/server/composables/useOrder.ts @@ -0,0 +1,35 @@ +export function useOrder() { + async function getReferenceDiscountAmountForUser(orderId: string, userId: string): Promise { + const order = await db.query.orderTable.findFirst({ + where(schema, { eq }) { + return eq(schema.id, orderId) + }, + with: { + package: true, + reference: true, + }, + }) + + if (!order || !order.package || !order.reference) return 0 + + const price = order.package.price + const reference = order.reference + + let discountedPrice = price + + if (reference.amount && reference.amount > 0) { + discountedPrice = price - reference.amount + } else if (reference.percentage && reference.percentage > 0) { + discountedPrice = price * (1 - reference.percentage / 100) + } + + const discountAmount = price - Math.max(0, Math.ceil(discountedPrice)) + + return discountAmount + } + + + return { + getReferenceDiscountAmountForUser + } +} diff --git a/server/composables/usePayment.ts b/server/composables/usePayment.ts new file mode 100644 index 00000000..847af9e5 --- /dev/null +++ b/server/composables/usePayment.ts @@ -0,0 +1,100 @@ +import { eq } from 'drizzle-orm' +import { PaymentStatus, orderTable, paymentProviderTransactionTable, paymentTable } from '../db/schemas' +import type { Order, Payment, PaymentProviderTransaction } from '../types/models' + +export function usePayment() { + async function createOrder(productId: string, userId: string, referenceId?: string): Promise { + return ( + await db.insert(orderTable).values({ + product_id: productId, + user_id: userId, + reference_id: referenceId, + }).returning() + )[0] + } + + async function createPayment(orderId: string, userId: string, amount: number): Promise { + return ( + await db.insert(paymentTable).values({ + amount, + status: PaymentStatus.PENDING, + user_id: userId, + order_id: orderId, + }).returning() + )[0] + } + + function updatePaymentStatus(paymentId: string, status: PaymentStatus) { + return db.update(paymentTable).set({ + status, + }).where( + eq(paymentTable.id, paymentId), + ) + } + + async function createProviderTransaction( + paymentId: string, + userId: string, + orderCode: string, + provider: string, + productType: string, + productInfo: Record, + ): Promise { + return ( + await db.insert(paymentProviderTransactionTable).values({ + provider, + provider_transaction_id: orderCode, + provider_transaction_status: PaymentStatus.PENDING, + provider_transaction_info: `${productType}:${productInfo.amount}`, + payment_id: paymentId, + user_id: userId, + }).returning() + )[0] + } + + function getProviderTransactionByOrderCode(orderCode: string): Promise< + | (PaymentProviderTransaction & { + payment: Payment & { + order: Order & { + reference: any + package: any + } + } + }) + | undefined + > { + return db.query.paymentProviderTransactionTable.findFirst({ + where: eq(paymentProviderTransactionTable.provider_transaction_id, orderCode), + with: { + payment: { + with: { + order: { + with: { + reference: true, + package: true, + }, + }, + }, + }, + }, + }) + } + + function updateProviderTransactionStatus(transactionId: string, status: PaymentStatus, resolvedAt: string | Date | number) { + return db.update(paymentProviderTransactionTable).set({ + provider_transaction_status: status, + provider_transaction_resolved_at: new Date(resolvedAt), + }).where( + eq(paymentProviderTransactionTable.id, transactionId), + ) + } + + return { + createOrder, + createPayment, + updatePaymentStatus, + createProviderTransaction, + getProviderTransactionByOrderCode, + updateProviderTransactionStatus, + } +} diff --git a/server/composables/useProduct.ts b/server/composables/useProduct.ts new file mode 100644 index 00000000..64f98e17 --- /dev/null +++ b/server/composables/useProduct.ts @@ -0,0 +1,56 @@ +import type { Product } from '@base/server/types/models' +import { ProductStatus, ProductType } from '../db/schemas' + +export function useProduct() { + function getProducts(): Promise { + return db.query.productTable.findMany({ + where(schema, { eq }) { + return eq(schema.status, ProductStatus.ACTIVE) + }, + orderBy(schema, { asc }) { + return [ + asc(schema.position), + ] + }, + }) + } + + function getCreditPackages(): Promise { + return db.query.productTable.findMany({ + where(schema, { eq, and }) { + return and( + eq(schema.type, ProductType.CREDIT), + eq(schema.status, ProductStatus.ACTIVE), + ) + }, + orderBy(schema, { asc }) { + return [ + asc(schema.position), + ] + }, + }) + } + + function getProductByProductId(productId: string): Promise | undefined> { + return db.query.productTable.findFirst({ + where(schema, { eq, and }) { + return and( + eq(schema.id, productId), + eq(schema.status, ProductStatus.ACTIVE), + ) + }, + columns: { + id: true, + price: true, + price_discount: true, + amount: true, + }, + }) + } + + return { + getProducts, + getCreditPackages, + getProductByProductId, + } +} diff --git a/server/composables/useReference.ts b/server/composables/useReference.ts new file mode 100644 index 00000000..208b5c64 --- /dev/null +++ b/server/composables/useReference.ts @@ -0,0 +1,227 @@ +import { eq, sql, and, isNull, count, or, gt } from 'drizzle-orm' +import { referenceTable, referenceUsageTable, userTable } from '../db/schemas' +import type { PaginatedResponse, PaginationOptions } from '@base/server/types/models' + +export const REFERENCE_CODE_COOKIE_NAME = 'referCode' + +export function useReference() { + function getReferenceById(referenceId: string) { + return db.query.referenceTable.findFirst({ + where(schema, { eq }) { + return eq(schema.id, referenceId) + }, + }) + } + + function getReferenceByCode(referenceCode: string) { + return db.query.referenceTable.findFirst({ + where(schema, { eq }) { + return eq(schema.code, referenceCode) + }, + }) + } + + function getUserReferenceUsage(userId: string) { + return db.query.referenceUsageTable.findFirst({ + where(schema, { eq }) { + return eq(schema.user_id, userId) + }, + }) + } + + async function getUserBestPrice(userId: string, originalPrice: number, discountPrice?: number | null, referCode?: string | null) { + const userReferenceUsage = await getUserReferenceUsage(userId) + + let price = originalPrice + + if (!userReferenceUsage && referCode) { + const reference = await getReferenceByCode(referCode) + + if (reference) { + const referenceInStock = reference.quantity === null || reference.quantity > 0 + + if (referenceInStock && reference?.percentage) { + price = originalPrice * (1 - reference.percentage / 100) + } + else if (referenceInStock && reference?.amount) { + price = originalPrice - reference.amount + } + } + } + + // use the best price for the customer + price = Math.ceil( + discountPrice + ? Math.min(discountPrice, price) + : price, + ) + + // clamp to minimum of 10000 + price = Math.max(price, 10000); + + if (!price) { + throw createError({ + statusCode: 400, + statusMessage: ErrorMessage.BAD_REQUEST, + }) + } + + return price + } + + async function createReferenceUsage(userId: string, referenceId: string, paymentProviderTransactionId: string) { + const referenceUsage = await db.insert(referenceUsageTable).values({ + user_id: userId, + reference_id: referenceId, + payment_provider_transaction_id: paymentProviderTransactionId, + }).returning() + + await db.update(referenceTable) + .set({ + quantity: sql`${referenceTable.quantity} - 1`, + }) + .where(eq(referenceTable.id, referenceId)) + + return referenceUsage[0] + } + + async function createReference({ + userId, + percentage, + amount, + quantity, + }: { + userId: string + percentage?: number + amount?: number + quantity?: number | null + }) { + + const reference = await db.insert(referenceTable).values({ + user_id: userId, + percentage: percentage, + amount: amount, + quantity: quantity ?? null, + }).returning() + + return reference[0] + } + + async function deleteReferenceByUserId(userId: string) { + const deletedReferences = await db + .delete(referenceTable) + .where(eq(referenceTable.user_id, userId)) + .returning(); + + return deletedReferences; + } + + async function getAvailableReferencesByUserId(userId: string) { + const references = await db + .select() + .from(referenceTable) + .where(and( + eq(referenceTable.user_id, userId), + or( + isNull(referenceTable.quantity), + gt(referenceTable.quantity, 0) + ) + )) + + return references + } + + async function isReferenceUsableByUser(refCode: string, userId: string): Promise { + const reference = await db.query.referenceTable.findFirst({ + where(schema, { eq }) { + return eq(schema.code, refCode) + }, + }) + + if (!reference) return false + + const userUsedAnyRef = await db.query.referenceUsageTable.findFirst({ + where(schema, { eq }) { + return eq(schema.user_id, userId) + }, + }) + + if (userUsedAnyRef) return false + + const [{ count: usedCount }] = await db + .select({ count: count() }) + .from(referenceUsageTable) + .where(eq(referenceUsageTable.reference_id, reference.id)) + + const quantity = reference.quantity + + if (quantity === null) return true + + if (usedCount >= quantity) return false + + return true + } + + async function getReferenceUsageHistoryByUser( + userId: string, + options: Partial, + ): Promise> { + const limit = options.limit ?? 20 + const page = options.page ?? 1 + + const query = db + .select({ + referenceUsageId: referenceUsageTable.id, + paymentProviderTransactionId: referenceUsageTable.payment_provider_transaction_id, + usedAt: referenceUsageTable.created_at, + + usedByUserId: referenceUsageTable.user_id, + usedByUserName: userTable.name, + usedByUserEmail: userTable.primary_email, + + referenceId: referenceTable.id, + referenceCode: referenceTable.code, + referencePercentage: referenceTable.percentage, + referenceAmount: referenceTable.amount, + }) + .from(referenceUsageTable) + .innerJoin(referenceTable, eq(referenceUsageTable.reference_id, referenceTable.id)) + .innerJoin(userTable, eq(referenceUsageTable.user_id, userTable.id)) + .where(eq(referenceTable.user_id, userId)) + .orderBy( + options.sortAsc + ? sql.raw(`${options.sortBy || 'created_at'} ASC`) + : sql.raw(`${options.sortBy || 'created_at'} DESC`), + ) + .limit(limit) + .offset(limit * (page - 1)) + + const data = await query + + const total = options.withCount + ? ( + await db + .select({ count: count() }) + .from(referenceUsageTable) + .innerJoin(referenceTable, eq(referenceUsageTable.reference_id, referenceTable.id)) + .where(eq(referenceTable.user_id, userId)) + )[0].count + : data.length + + return { data, total } + } + + + return { + getReferenceById, + getUserReferenceUsage, + getUserBestPrice, + createReferenceUsage, + createReference, + deleteReferenceByUserId, + getAvailableReferencesByUserId, + isReferenceUsableByUser, + getReferenceByCode, + getReferenceUsageHistoryByUser + } +} diff --git a/server/composables/useShortcutCrud.ts b/server/composables/useShortcutCrud.ts deleted file mode 100644 index e4e6e7e4..00000000 --- a/server/composables/useShortcutCrud.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { eq } from 'drizzle-orm' -import type { ParsedFilterQuery } from '@base/server/utils/filter' -import { userShortcutTable } from '@base/server/db/schemas' -import { useCrud } from './useCrud' - -export function useShortcutCrud(userId: string) { - const { - getRecordsPaginated, - getRecordByKey, - createRecord, - updateRecordByKey, - deleteRecordByKey, - countRecords, - } = useCrud(userShortcutTable, { - searchBy: ['route'], - queryRestrict: () => eq(userShortcutTable.user_id, userId), - }) - - async function getShortcutsPaginated(options: ParsedFilterQuery) { - const { data, total } = await getRecordsPaginated(options) - - return { data, total } - } - - async function getShortcutById(id: string) { - const { data } = await getRecordByKey('id', id) - - return { data } - } - - async function updateShortcutById(id: string, body: any) { - const { data } = await updateRecordByKey('id', id, body) - - return { data } - } - - async function createShortcut(body: any) { - const { data } = await createRecord(body) - - return { data } - } - - async function deleteShortcutById(id: string) { - const { data } = await deleteRecordByKey('id', id) - - return { data } - } - - function countShortcuts() { - return countRecords() - } - - return { - getShortcutsPaginated, - getShortcutById, - createShortcut, - updateShortcutById, - deleteShortcutById, - countShortcuts, - } -} diff --git a/server/composables/useUser.ts b/server/composables/useUser.ts new file mode 100644 index 00000000..a3ac6692 --- /dev/null +++ b/server/composables/useUser.ts @@ -0,0 +1,139 @@ +import { eq } from 'drizzle-orm' +import { userTable } from '../db/schemas' +import type { User, UserInput } from '../types/models' + +export function useUser() { + async function getUserById(userId: string): Promise { + return db.query.userTable.findFirst({ + where: isUUID(userId) + ? eq(userTable.id, userId) + : eq(userTable.logto_id, userId), + }) + } + + async function getUserCreditById(userId: string): Promise { + const userProfile = await getUserById(userId) + + return userProfile?.credit || 0 + } + + async function createUser(userId: string, payload: UserInput): Promise { + if (!userId) { + throw new Error('User ID is required when creating a user') + } + + // Remove null values to avoid type errors + const cleanPayload: Record = {} + + for (const [key, value] of Object.entries(payload)) { + if (value !== null && value !== undefined) { + cleanPayload[key] = value + } + } + + const insertData: Record = { + ...cleanPayload, + } + + // Set the ID field properly + if (isUUID(userId)) { + insertData.id = userId + } + else { + // If not UUID, logto_id is required + insertData.logto_id = userId + } + + // Type assertion to satisfy TypeScript + return ( + await db.insert(userTable) + .values(insertData as any) + .returning() + )[0] + } + + async function updateUser(userId: string, payload: Partial): Promise { + // Remove null values to avoid type errors + const cleanPayload: Record = { + updated_at: new Date(), + } + + for (const [key, value] of Object.entries(payload)) { + if (value !== null && value !== undefined) { + cleanPayload[key] = value + } + } + + return ( + await db.update(userTable) + .set(cleanPayload) + .where( + isUUID(userId) + ? eq(userTable.id, userId) + : eq(userTable.logto_id, userId), + ) + .returning() + )[0] + } + + function upsertUser(userId: string, payload: UserInput): Promise { + return db.transaction(async () => { + // Resolve to correct ID format + const resolvedUser = await getUserById(userId) + + if (!resolvedUser) { + // If we can't resolve to UUID, create new user with Logto ID reference + return createUser(userId, payload) + } + + // Update existing user + return updateUser(userId, payload) + }) + } + + function deleteUser(userId: string) { + return db.delete(userTable) + .where( + isUUID(userId) + ? eq(userTable.id, userId) + : eq(userTable.logto_id, userId), + ) + } + + function updateLastSignIn(userId: string, signInTime?: Date | number | string) { + return db.update(userTable) + .set({ + last_sign_in_at: signInTime ? new Date(signInTime) : new Date(), + updated_at: new Date(), + }) + .where( + isUUID(userId) + ? eq(userTable.id, userId) + : eq(userTable.logto_id, userId), + ) + } + + function updateSuspensionStatus(userId: string, isSuspended: boolean) { + return db.update(userTable) + .set({ + is_suspended: isSuspended, + updated_at: new Date(), + }) + .where( + isUUID(userId) + ? eq(userTable.id, userId) + : eq(userTable.logto_id, userId), + ) + } + + return { + getUserById, + getUserCreditById, + createUser, + updateUser, + upsertUser, + deleteUser, + updateLastSignIn, + updateSuspensionStatus, + } +} diff --git a/docker/.gitkeep b/server/composables/useUserDevice.ts similarity index 100% rename from docker/.gitkeep rename to server/composables/useUserDevice.ts diff --git a/server/composables/useUserDeviceCrud.ts b/server/composables/useUserDeviceCrud.ts deleted file mode 100644 index 3493f3bb..00000000 --- a/server/composables/useUserDeviceCrud.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { and, eq } from 'drizzle-orm' -import { userDeviceTable } from '@base/server/db/schemas' -import { useCrud } from './useCrud' - -interface QueryRestrict { - user_id: string | any -} -export function useUserDeviceCrud(queryRestrict: QueryRestrict) { - const { getRecordsPaginated, getRecordByKey, createRecord, deleteRecordByKey } = useCrud(userDeviceTable, { - queryRestrict: () => and( - ...[queryRestrict.user_id && eq(userDeviceTable.user_id, queryRestrict.user_id)].filter(Boolean), - ), - }) - async function getUserDeviceAllTokens(options: ParsedFilterQuery) { - const { data, total } = await getRecordsPaginated(options) - return { data, total } - } - async function getUserDeviceToken(token_device: string) { - const { data } = await getRecordByKey('token_device', token_device) - return { data } - } - - async function createUserDeviceToken(body: any) { - const { data } = await createRecord(body) - return { data } - } - async function deleteUserDeviceToken(token_device: string) { - const { data } = await deleteRecordByKey('token_device', token_device) - return { data } - } - - return { - getUserDeviceToken, - createUserDeviceToken, - deleteUserDeviceToken, - getUserDeviceAllTokens, - } -} diff --git a/server/db/schemas/credit_histories.schema.ts b/server/db/schemas/credit_histories.schema.ts index d9514e86..3547fe37 100644 --- a/server/db/schemas/credit_histories.schema.ts +++ b/server/db/schemas/credit_histories.schema.ts @@ -1,10 +1,20 @@ -import { numeric, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' +import { integer, pgTable, timestamp, uuid } from 'drizzle-orm/pg-core' +import { relations } from 'drizzle-orm/relations' import { creditHistoryType } from './enum.schema' +import { userTable } from './users.schema' export const creditHistoryTable = pgTable('credit_histories', { id: uuid('id').defaultRandom().primaryKey().notNull(), - amount: numeric('amount').notNull(), + amount: integer('amount').notNull(), type: creditHistoryType('type').notNull(), - user_id: text('user_id').notNull(), + user_id: uuid('user_id') + .references(() => userTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }).notNull(), created_at: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), }) + +export const creditHistoryRelations = relations(creditHistoryTable, ({ one }) => ({ + user: one(userTable, { + fields: [creditHistoryTable.user_id], + references: [userTable.id], + }), +})) diff --git a/server/db/schemas/credit_packages.schema.ts b/server/db/schemas/credit_packages.schema.ts deleted file mode 100644 index 079643bd..00000000 --- a/server/db/schemas/credit_packages.schema.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { jsonb, numeric, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' - -export const creditPackageTable = pgTable('credit_packages', { - id: uuid('id').defaultRandom().primaryKey().notNull(), - title: text('title'), - description: text('description'), - price: numeric('price').notNull(), - currency: text('currency').notNull(), - amount: numeric('amount').notNull(), - features: jsonb('features'), - created_at: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - updated_at: timestamp('updated_at', { withTimezone: true }).defaultNow().$onUpdate(() => new Date()), -}) diff --git a/server/db/schemas/devices.schema.ts b/server/db/schemas/devices.schema.ts new file mode 100644 index 00000000..4a7fea57 --- /dev/null +++ b/server/db/schemas/devices.schema.ts @@ -0,0 +1,18 @@ +import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' +import { relations } from 'drizzle-orm/relations' +import { userTable } from './users.schema' + +export const deviceTable = pgTable('devices', { + id: uuid('id').defaultRandom().primaryKey().notNull(), + user_id: uuid('user_id') + .references(() => userTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }).notNull(), + token_device: text('token_device'), + created_at: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), +}) + +export const deviceRelations = relations(deviceTable, ({ one }) => ({ + user: one(userTable, { + fields: [deviceTable.user_id], + references: [userTable.id], + }), +})) diff --git a/server/db/schemas/enum.schema.ts b/server/db/schemas/enum.schema.ts index 2fd4eb64..eb780ed9 100644 --- a/server/db/schemas/enum.schema.ts +++ b/server/db/schemas/enum.schema.ts @@ -21,8 +21,23 @@ export enum SupportedCurrency { VND = 'VND', } +export enum ProductType { + CREDIT = 'credit', + SUBSCRIPTION = 'subscription', + OTHER = 'other', +} + +export enum ProductStatus { + ACTIVE = 'active', + INACTIVE = 'inactive', +} + export const paymentStatus = pgEnum('payment_status', enumToPgEnum(PaymentStatus)) export const creditHistoryType = pgEnum('credit_history_type', enumToPgEnum(CreditHistoryType)) export const supportedCurrency = pgEnum('supported_currency', enumToPgEnum(SupportedCurrency)) + +export const productType = pgEnum('product_type', enumToPgEnum(ProductType)) + +export const productStatus = pgEnum('product_status', enumToPgEnum(ProductStatus)) diff --git a/server/db/schemas/identities.schema.ts b/server/db/schemas/identities.schema.ts new file mode 100644 index 00000000..f4ecc188 --- /dev/null +++ b/server/db/schemas/identities.schema.ts @@ -0,0 +1,21 @@ +import { jsonb, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' +import { relations } from 'drizzle-orm/relations' +import { userTable } from './users.schema' + +export const identityTable = pgTable('identities', { + id: uuid('id').defaultRandom().primaryKey().notNull(), + user_id: uuid('user_id') + .references(() => userTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }).notNull(), + provider: text('provider').notNull(), // e.g., 'google', 'facebook', etc. + provider_user_id: text('provider_user_id').notNull(), + provider_data: jsonb('provider_data').default({}), + created_at: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updated_at: timestamp('updated_at', { withTimezone: true }).defaultNow().$onUpdate(() => new Date()), +}) + +export const identityRelations = relations(identityTable, ({ one }) => ({ + user: one(userTable, { + fields: [identityTable.user_id], + references: [userTable.id], + }), +})) diff --git a/server/db/schemas/index.ts b/server/db/schemas/index.ts index e6c9ce0e..226cb891 100644 --- a/server/db/schemas/index.ts +++ b/server/db/schemas/index.ts @@ -1,21 +1,23 @@ export * from './credit_histories.schema' -export * from './credit_packages.schema' +export * from './products.schema' export * from './enum.schema' +export * from './identities.schema' + export * from './payment_provider_transactions.schema' -export * from './sys_faq_categories.schema' +export * from './notifications.schema' -export * from './sys_faqs.schema' +export * from './devices.schema' -export * from './sys_notifications.schema' +export * from './orders.schema' -export * from './user_devices.schema' +export * from './payments.schema' -export * from './user_orders.schema' +export * from './users.schema' -export * from './user_payments.schema' +export * from './reference_usages.schema' -export * from './user_shortcuts.schema' +export * from './references.schema' diff --git a/server/db/schemas/notifications.schema.ts b/server/db/schemas/notifications.schema.ts new file mode 100644 index 00000000..e53e3de1 --- /dev/null +++ b/server/db/schemas/notifications.schema.ts @@ -0,0 +1,21 @@ +import { jsonb, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' +import { relations } from 'drizzle-orm/relations' +import { userTable } from './users.schema' + +export const notificationTable = pgTable('notifications', { + id: uuid('id').defaultRandom().primaryKey().notNull(), + created_at: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + title: text('title'), + message: text('message'), + action: jsonb('action'), + read_at: timestamp('read_at', { withTimezone: true }), + user_id: uuid('user_id') + .references(() => userTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }).notNull(), +}) + +export const notificationRelations = relations(notificationTable, ({ one }) => ({ + user: one(userTable, { + fields: [notificationTable.user_id], + references: [userTable.id], + }), +})) diff --git a/server/db/schemas/orders.schema.ts b/server/db/schemas/orders.schema.ts new file mode 100644 index 00000000..5a2381e2 --- /dev/null +++ b/server/db/schemas/orders.schema.ts @@ -0,0 +1,37 @@ +import { pgTable, timestamp, uuid } from 'drizzle-orm/pg-core' +import { relations } from 'drizzle-orm/relations' +import { paymentTable } from './payments.schema' +import { productTable } from './products.schema' +import { userTable } from './users.schema' +import { referenceTable } from './references.schema' + +export const orderTable = pgTable('orders', { + id: uuid('id').defaultRandom().primaryKey().notNull(), + user_id: uuid('user_id') + .references(() => userTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }).notNull(), + product_id: uuid('product_id') + .references(() => productTable.id, { onDelete: 'no action', onUpdate: 'no action' }), + reference_id: uuid('reference_id') + .references(() => referenceTable.id, { onDelete: 'no action', onUpdate: 'no action' }), + created_at: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updated_at: timestamp('updated_at', { withTimezone: true }).defaultNow().$onUpdate(() => new Date()), +}) + +export const userOrderRelations = relations(orderTable, ({ one }) => ({ + package: one(productTable, { + fields: [orderTable.product_id], + references: [productTable.id], + }), + payment: one(paymentTable, { + fields: [orderTable.id], + references: [paymentTable.order_id], + }), + user: one(userTable, { + fields: [orderTable.user_id], + references: [userTable.id], + }), + reference: one(referenceTable, { + fields: [orderTable.reference_id], + references: [referenceTable.id], + }), +})) diff --git a/server/db/schemas/payment_provider_transactions.schema.ts b/server/db/schemas/payment_provider_transactions.schema.ts index 8f260106..82320f5d 100644 --- a/server/db/schemas/payment_provider_transactions.schema.ts +++ b/server/db/schemas/payment_provider_transactions.schema.ts @@ -1,6 +1,7 @@ import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' import { relations } from 'drizzle-orm/relations' -import { userPaymentTable } from './user_payments.schema' +import { paymentTable } from './payments.schema' +import { userTable } from './users.schema' export const paymentProviderTransactionTable = pgTable('payment_provider_transactions', { id: uuid('id').defaultRandom().primaryKey().notNull(), @@ -10,15 +11,20 @@ export const paymentProviderTransactionTable = pgTable('payment_provider_transac provider_transaction_resolved_at: timestamp('provider_transaction_resolved_at', { withTimezone: true }), // vnp_PayDate provider_transaction_info: text('provider_transaction_info').notNull(), // vnp_OrderInfo payment_id: uuid('payment_id') - .references(() => userPaymentTable.id, { onDelete: 'no action', onUpdate: 'no action' }).notNull(), - user_id: text('user_id').notNull(), + .references(() => paymentTable.id, { onDelete: 'no action', onUpdate: 'no action' }).notNull(), + user_id: uuid('user_id') + .references(() => userTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }).notNull(), created_at: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updated_at: timestamp('updated_at', { withTimezone: true }).defaultNow().$onUpdate(() => new Date()), }) export const paymentProviderTransactionRelations = relations(paymentProviderTransactionTable, ({ one }) => ({ - payment: one(userPaymentTable, { + payment: one(paymentTable, { fields: [paymentProviderTransactionTable.payment_id], - references: [userPaymentTable.id], + references: [paymentTable.id], + }), + user: one(userTable, { + fields: [paymentProviderTransactionTable.user_id], + references: [userTable.id], }), })) diff --git a/server/db/schemas/payments.schema.ts b/server/db/schemas/payments.schema.ts new file mode 100644 index 00000000..e08b1732 --- /dev/null +++ b/server/db/schemas/payments.schema.ts @@ -0,0 +1,33 @@ +import { integer, pgTable, timestamp, uuid } from 'drizzle-orm/pg-core' +import { relations } from 'drizzle-orm/relations' +import { orderTable } from './orders.schema' +import { paymentStatus } from './enum.schema' +import { paymentProviderTransactionTable } from './payment_provider_transactions.schema' +import { userTable } from './users.schema' + +export const paymentTable = pgTable('payments', { + id: uuid('id').defaultRandom().primaryKey().notNull(), + amount: integer('amount').notNull(), + status: paymentStatus('status').notNull(), + order_id: uuid('order_id') + .references(() => orderTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }).notNull(), + user_id: uuid('user_id') + .references(() => userTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }).notNull(), + created_at: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updated_at: timestamp('updated_at', { withTimezone: true }).defaultNow().$onUpdate(() => new Date()), +}) + +export const userPaymentRelations = relations(paymentTable, ({ one }) => ({ + order: one(orderTable, { + fields: [paymentTable.order_id], + references: [orderTable.id], + }), + providerTransaction: one(paymentProviderTransactionTable, { + fields: [paymentTable.id], + references: [paymentProviderTransactionTable.payment_id], + }), + user: one(userTable, { + fields: [paymentTable.user_id], + references: [userTable.id], + }), +})) diff --git a/server/db/schemas/products.schema.ts b/server/db/schemas/products.schema.ts new file mode 100644 index 00000000..e2cf4f14 --- /dev/null +++ b/server/db/schemas/products.schema.ts @@ -0,0 +1,29 @@ +import { integer, jsonb, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' +import { relations } from 'drizzle-orm/relations' +import { orderTable } from './orders.schema' +import { ProductType, productStatus, productType } from './enum.schema' + +interface PricingPlanFeature { + title: string + icon?: string +} + +export const productTable = pgTable('products', { + id: uuid('id').defaultRandom().primaryKey().notNull(), + title: text('title'), + description: text('description'), + price: integer('price').notNull(), + price_discount: integer('price_discount'), + currency: text('currency').notNull(), + amount: integer('amount').notNull(), + type: productType('type').default(ProductType.CREDIT).notNull(), + features: jsonb('features').$type().default([]), + position: integer('position'), + status: productStatus('status').default('active').notNull(), + created_at: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updated_at: timestamp('updated_at', { withTimezone: true }).defaultNow().$onUpdate(() => new Date()), +}) + +export const productRelations = relations(productTable, ({ many }) => ({ + orders: many(orderTable), +})) diff --git a/server/db/schemas/reference_usages.schema.ts b/server/db/schemas/reference_usages.schema.ts new file mode 100644 index 00000000..8cd10369 --- /dev/null +++ b/server/db/schemas/reference_usages.schema.ts @@ -0,0 +1,25 @@ +import { pgTable, timestamp, uuid } from 'drizzle-orm/pg-core' +import { relations } from 'drizzle-orm/relations' +import { userTable } from './users.schema' +import { referenceTable } from './references.schema' +import { paymentProviderTransactionTable } from './payment_provider_transactions.schema' + +export const referenceUsageTable = pgTable('reference_usages', { + id: uuid('id').defaultRandom().primaryKey().notNull(), + // the one who uses/applies the reference code + user_id: uuid('user_id') + .references(() => userTable.id, { onDelete: 'no action', onUpdate: 'no action' }).notNull(), + reference_id: uuid('reference_id') + .references(() => referenceTable.id, { onDelete: 'no action', onUpdate: 'no action' }).notNull(), + payment_provider_transaction_id: uuid('payment_provider_transaction_id') + .references(() => paymentProviderTransactionTable.id, { onDelete: 'no action', onUpdate: 'no action' }).notNull(), + created_at: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updated_at: timestamp('updated_at', { withTimezone: true }).defaultNow().$onUpdate(() => new Date()), +}) + +export const referenceUsageRelations = relations(referenceUsageTable, ({ one }) => ({ + user: one(userTable, { + fields: [referenceUsageTable.user_id], + references: [userTable.id], + }), +})) diff --git a/server/db/schemas/references.schema.ts b/server/db/schemas/references.schema.ts new file mode 100644 index 00000000..1631f879 --- /dev/null +++ b/server/db/schemas/references.schema.ts @@ -0,0 +1,24 @@ +import { integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' +import { relations } from 'drizzle-orm/relations' +import { customAlphabet } from 'nanoid' +import { userTable } from './users.schema' + +export const referenceTable = pgTable('references', { + id: uuid('id').defaultRandom().primaryKey().notNull(), + // owner, the one who created the reference code + user_id: uuid('user_id') + .references(() => userTable.id, { onDelete: 'no action', onUpdate: 'no action' }).notNull(), + code: text('code').$defaultFn(() => customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 16)()), + percentage: integer('percentage').default(0), + amount: integer('amount').default(0), // discount amount (in VND) + quantity: integer('quantity'), // number of times this reference code can be used + created_at: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updated_at: timestamp('updated_at', { withTimezone: true }).defaultNow().$onUpdate(() => new Date()), +}) + +export const referenceRelations = relations(referenceTable, ({ one }) => ({ + user: one(userTable, { + fields: [referenceTable.user_id], + references: [userTable.id], + }), +})) diff --git a/server/db/schemas/sys_faq_categories.schema.ts b/server/db/schemas/sys_faq_categories.schema.ts deleted file mode 100644 index e46a8bb4..00000000 --- a/server/db/schemas/sys_faq_categories.schema.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { pgTable, smallint, text, timestamp } from 'drizzle-orm/pg-core' -import { relations } from 'drizzle-orm/relations' -import { sysFaqTable } from './sys_faqs.schema' - -export const sysFaqCategoryTable = pgTable('sys_faq_categories', { - id: smallint('id').primaryKey().generatedByDefaultAsIdentity({ name: 'sys_faq_categories_id_seq', startWith: 1, increment: 1, minValue: 1, maxValue: 32767, cache: 1 }), - title: text('title'), - icon: text('icon'), - subtitle: text('subtitle'), - created_at: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), -}) - -export const sysFaqCategoryRelations = relations(sysFaqCategoryTable, ({ many }) => ({ - faqs: many(sysFaqTable), -})) diff --git a/server/db/schemas/sys_faqs.schema.ts b/server/db/schemas/sys_faqs.schema.ts deleted file mode 100644 index 3bc4036f..00000000 --- a/server/db/schemas/sys_faqs.schema.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { pgTable, smallint, text, timestamp, uuid } from 'drizzle-orm/pg-core' -import { relations } from 'drizzle-orm/relations' -import { sysFaqCategoryTable } from './sys_faq_categories.schema' - -export const sysFaqTable = pgTable('sys_faqs', { - id: uuid('id').defaultRandom().primaryKey().notNull(), - answer: text('answer'), - category_id: smallint('category_id') - .references(() => sysFaqCategoryTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }), - question: text('question'), - created_at: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), -}) - -export const sysFaqRelations = relations(sysFaqTable, ({ one }) => ({ - category: one(sysFaqCategoryTable, { - fields: [sysFaqTable.category_id], - references: [sysFaqCategoryTable.id], - }), -})) diff --git a/server/db/schemas/sys_notifications.schema.ts b/server/db/schemas/sys_notifications.schema.ts deleted file mode 100644 index 4d176ee7..00000000 --- a/server/db/schemas/sys_notifications.schema.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { jsonb, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' - -export const sysNotificationTable = pgTable('sys_notifications', { - id: uuid('id').defaultRandom().primaryKey().notNull(), - created_at: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - title: text('title'), - message: text('message'), - action: jsonb('action'), - read_at: timestamp('read_at', { withTimezone: true }), - user_id: text('user_id').notNull(), -}) diff --git a/server/db/schemas/user_devices.schema.ts b/server/db/schemas/user_devices.schema.ts deleted file mode 100644 index 98f1b0dc..00000000 --- a/server/db/schemas/user_devices.schema.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' - -export const userDeviceTable = pgTable('user_devices', { - id: uuid('id').defaultRandom().primaryKey().notNull(), - user_id: text('user_id').notNull(), - token_device: text('token_device'), - created_at: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), -}) diff --git a/server/db/schemas/user_orders.schema.ts b/server/db/schemas/user_orders.schema.ts deleted file mode 100644 index 3a188d4d..00000000 --- a/server/db/schemas/user_orders.schema.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' -import { relations } from 'drizzle-orm/relations' -import { userPaymentTable } from './user_payments.schema' - -export const userOrderTable = pgTable('user_orders', { - id: uuid('id').defaultRandom().primaryKey().notNull(), - user_id: text('user_id').notNull(), - created_at: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - updated_at: timestamp('updated_at', { withTimezone: true }).defaultNow().$onUpdate(() => new Date()), -}) - -export const userOrderRelations = relations(userOrderTable, ({ one }) => ({ - payment: one(userPaymentTable, { - fields: [userOrderTable.id], - references: [userPaymentTable.order_id], - }), -})) diff --git a/server/db/schemas/user_payments.schema.ts b/server/db/schemas/user_payments.schema.ts deleted file mode 100644 index 9e651d88..00000000 --- a/server/db/schemas/user_payments.schema.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { numeric, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' -import { relations } from 'drizzle-orm/relations' -import { userOrderTable } from './user_orders.schema' -import { paymentStatus } from './enum.schema' -import { paymentProviderTransactionTable } from './payment_provider_transactions.schema' - -export const userPaymentTable = pgTable('user_payments', { - id: uuid('id').defaultRandom().primaryKey().notNull(), - amount: numeric('amount').notNull(), - status: paymentStatus('status').notNull(), - order_id: uuid('order_id') - .references(() => userOrderTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }).notNull(), - user_id: text('user_id').notNull(), - created_at: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - updated_at: timestamp('updated_at', { withTimezone: true }).defaultNow().$onUpdate(() => new Date()), -}) - -export const userPaymentRelations = relations(userPaymentTable, ({ one }) => ({ - order: one(userOrderTable, { - fields: [userPaymentTable.order_id], - references: [userOrderTable.id], - }), - providerTransaction: one(paymentProviderTransactionTable, { - fields: [userPaymentTable.id], - references: [paymentProviderTransactionTable.payment_id], - }), -})) diff --git a/server/db/schemas/user_shortcuts.schema.ts b/server/db/schemas/user_shortcuts.schema.ts deleted file mode 100644 index dca5b33c..00000000 --- a/server/db/schemas/user_shortcuts.schema.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { pgTable, text, uuid } from 'drizzle-orm/pg-core' - -export const userShortcutTable = pgTable('user_shortcuts', { - id: uuid('id').defaultRandom().primaryKey().notNull(), - route: text('route').notNull(), - user_id: text('user_id').notNull(), -}) diff --git a/server/db/schemas/users.schema.ts b/server/db/schemas/users.schema.ts new file mode 100644 index 00000000..d943456b --- /dev/null +++ b/server/db/schemas/users.schema.ts @@ -0,0 +1,55 @@ +import { boolean, integer, jsonb, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' +import { relations } from 'drizzle-orm/relations' +import { identityTable } from './identities.schema' +import { deviceTable } from './devices.schema' +import { creditHistoryTable } from './credit_histories.schema' +import { notificationTable } from './notifications.schema' +import { orderTable } from './orders.schema' +import { paymentTable } from './payments.schema' +import { referenceTable } from './references.schema' +import { referenceUsageTable } from './reference_usages.schema' + +export const userTable = pgTable('users', { + id: uuid('id').defaultRandom().primaryKey().notNull(), + logto_id: text('logto_id').notNull().unique(), + + // User profile data + username: text('username'), + name: text('name'), + primary_email: text('primary_email'), + primary_phone: text('primary_phone'), + avatar: text('avatar'), + + // Custom fields from your original profiles schema + facebook: text('facebook'), + zalo: text('zalo'), + credit: integer('credit').default(0), + + // Notification settings with default values + email_notifications: boolean('email_notifications').default(true), + desktop_notifications: boolean('desktop_notifications').default(true), + product_updates_notifications: boolean('product_updates_notifications').default(true), + weekly_digest_notifications: boolean('weekly_digest_notifications').default(true), + important_updates_notifications: boolean('important_updates_notifications').default(true), + + // Additional Logto metadata + custom_data: jsonb('custom_data').default({}), + last_sign_in_at: timestamp('last_sign_in_at', { withTimezone: true }), + is_suspended: boolean('is_suspended').default(false), + has_password: boolean('has_password').default(false), + + // Timestamps + created_at: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updated_at: timestamp('updated_at', { withTimezone: true }).defaultNow().$onUpdate(() => new Date()), +}) + +export const userRelations = relations(userTable, ({ many, one }) => ({ + identities: many(identityTable), + devices: many(deviceTable), + creditHistories: many(creditHistoryTable), + notifications: many(notificationTable), + orders: many(orderTable), + payments: many(paymentTable), + references: many(referenceTable), + referenceUsage: one(referenceUsageTable), +})) diff --git a/server/db/seeds/all.seed.ts b/server/db/seeds/all.seed.ts deleted file mode 100644 index cbf21400..00000000 --- a/server/db/seeds/all.seed.ts +++ /dev/null @@ -1,8 +0,0 @@ -export async function seed() { - try { - // - } - catch (error: any) { - console.error(error) - } -} diff --git a/server/db/seeds/index.seed.ts b/server/db/seeds/index.seed.ts deleted file mode 100644 index f025108b..00000000 --- a/server/db/seeds/index.seed.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { seed } from './all.seed' - -(async function () { - await seed() - - process.exit(0) -})() diff --git a/server/db/seeds/index.ts b/server/db/seeds/index.ts new file mode 100644 index 00000000..ca24c841 --- /dev/null +++ b/server/db/seeds/index.ts @@ -0,0 +1,10 @@ +import { seedProducts } from './products.seed' +import { seedNotifications } from './notifications.seed' + +(async () => { + await seedProducts() + + await seedNotifications('ax0p23zp6a2k', 'nguyenhuunguyeny.ny@gmail.com') + + process.exit(0) +})() diff --git a/server/db/seeds/notifications.seed.ts b/server/db/seeds/notifications.seed.ts new file mode 100644 index 00000000..b8e4be11 --- /dev/null +++ b/server/db/seeds/notifications.seed.ts @@ -0,0 +1,12 @@ +import { notificationTable } from '../schemas' +import { db } from '../../utils/db' + +export async function seedNotifications(id: string, email: string) { + console.log('Seeding notifications...') + const notifications = Array.from({ length: 10 }).map(() => ({ + user_id: id, + title: `Notification test ${Math.random()}`, + message: `Notification send to ${email}`, + })) + return await db.insert(notificationTable).values(notifications) +} diff --git a/server/db/seeds/products.seed.ts b/server/db/seeds/products.seed.ts new file mode 100644 index 00000000..1c38c66c --- /dev/null +++ b/server/db/seeds/products.seed.ts @@ -0,0 +1,126 @@ +import { productTable } from '../schemas' +import { db } from '../../utils/db' + +export async function seedProducts() { + console.log('Seeding products...') + + return await db.insert(productTable).values([ + { + title: 'Basic', + description: 'Ideal for testing their workflows.', + price: String(2000), + amount: String(30), + currency: 'VND', + position: String(1), + features: [ + 'Homelab servers', + '96.69% uptime', + 'Recommended for development purposes', + 'Customer support', + '1 vCPU, 1GB RAM, 8GB SSD', + 'Unlimited bandwidth', + 'Private VPS', + 'Free sub-domain', + ], + created_at: new Date(), + }, + { + title: 'Standard', + description: 'Great for small-scale automation needs.', + price: String(6900), + amount: String(69), + currency: 'VND', + position: String(2), + features: [ + 'Homelab servers', + '96.69% uptime', + 'Recommended for development purposes', + 'Customer support', + '1 vCPU, 2GB RAM, 15GB SSD', + 'Unlimited bandwidth', + 'Private VPS', + 'Custom domain', + ], + created_at: new Date(), + }, + { + title: 'Pro', + description: 'Enhanced performance for serious automation.', + price: String(9600), + amount: String(96), + currency: 'VND', + position: String(3), + features: [ + 'Homelab servers', + '96.69% uptime', + 'Recommended for development purposes', + 'Customer support', + '2 vCPU, 4GB RAM, 20GB SSD', + 'Unlimited bandwidth', + 'Private VPS', + 'Custom domain', + ], + created_at: new Date(), + }, + { + title: 'Premium', + description: 'Ultimate power for complex workflows.', + price: String(16900), + amount: String(169), + currency: 'VND', + position: String(4), + features: [ + 'Homelab servers', + '96.69% uptime', + 'Recommended for development purposes', + 'Customer support', + '4 vCPU, 8GB RAM, 25GB SSD', + 'Unlimited bandwidth', + 'Private VPS', + 'Custom domain', + ], + created_at: new Date(), + }, + { + title: 'Solution', + description: 'We solve your company problems by leveraging our expertise on n8n automation.', + price: String(0), + amount: String(0), + currency: 'VND', + position: String(5), + features: [ + 'Custom automation solutions', + 'Dedicated support team', + 'Tailored workflow design', + 'Integration with existing systems', + 'Scalable infrastructure', + 'Performance optimization', + 'Security and compliance', + 'Training and onboarding', + ], + created_at: new Date(), + }, + { + title: 'Enterprise', + description: 'Unlimited resource for your enterprise-scale automation.', + price: String(0), + amount: String(0), + currency: 'VND', + position: String(6), + features: [ + '99.99% uptime', + 'Recommended for enterprise purposes', + 'Priority customer support', + 'On-demand vCPU, RAM, SSD', + 'Unlimited bandwidth', + 'Private VPS', + 'Custom domain', + 'Scalable infrastructure', + 'Performance optimization', + 'Security and compliance', + 'Training and onboarding', + ], + created_at: new Date(), + }, + ]).returning() +} diff --git a/server/plugins/logger.ts b/server/plugins/logger.ts new file mode 100644 index 00000000..f99315ae --- /dev/null +++ b/server/plugins/logger.ts @@ -0,0 +1,14 @@ +import type { H3Event } from 'h3' + +const EXCLUDED_PATHS = ['/api/health'] + +export default defineNitroPlugin(async (nitroApp) => { + // Create a request logger function + const logRequest = logger.createRequestLogger() + + // Hook into each request + nitroApp.hooks.hook('request', async (event: H3Event) => { + if (!EXCLUDED_PATHS.includes(event.path)) + await logRequest(event) + }) +}) diff --git a/server/plugins/mongo.ts b/server/plugins/mongo.ts new file mode 100644 index 00000000..ffbf2d29 --- /dev/null +++ b/server/plugins/mongo.ts @@ -0,0 +1,15 @@ +import mongodbDriver from 'unstorage/drivers/mongodb' + +export default defineNitroPlugin(() => { + const storage = useStorage() + + if (process.env.MONGODB_CONNECTION_STRING && process.env.MONGODB_DATABASE_NAME && process.env.MONGODB_COLLECTION_NAME) { + const driver = mongodbDriver({ + connectionString: process.env.MONGODB_CONNECTION_STRING, + databaseName: process.env.MONGODB_DATABASE_NAME, + collectionName: process.env.MONGODB_COLLECTION_NAME, + }) + + storage.mount('mongodb', driver) + } +}) diff --git a/server/plugins/redis.ts b/server/plugins/redis.ts index 88c009ae..8bf52f0a 100644 --- a/server/plugins/redis.ts +++ b/server/plugins/redis.ts @@ -1,18 +1,27 @@ import redisDriver from 'unstorage/drivers/redis' +import upstashDriver from 'unstorage/drivers/upstash' export default defineNitroPlugin(() => { const storage = useStorage() - const config = useRuntimeConfig() - if (config.redis.host && config.redis.port && config.redis.password) { + if (process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN) { + const driver = upstashDriver({ + base: 'redis', + url: process.env.UPSTASH_REDIS_REST_URL, + token: process.env.UPSTASH_REDIS_REST_TOKEN, + }) + + storage.mount('redis', driver) + } + else if (process.env.REDIS_HOST && process.env.REDIS_PASSWORD) { const driver = redisDriver({ base: 'redis', - host: config.redis.host, - port: Number(config.redis.port), - password: config.redis.password, + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + password: process.env.REDIS_PASSWORD, + maxRetriesPerRequest: 0, }) - // Mount driver storage.mount('redis', driver) } }) diff --git a/server/api/users/.gitkeep b/server/tasks/.gitkeep similarity index 100% rename from server/api/users/.gitkeep rename to server/tasks/.gitkeep diff --git a/server/tasks/db/backup.ts b/server/tasks/db/backup.ts new file mode 100644 index 00000000..e9a8e75f --- /dev/null +++ b/server/tasks/db/backup.ts @@ -0,0 +1,156 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import { execa } from 'execa' +import { DeleteObjectsCommand, ListObjectsV2Command, PutObjectCommand } from '@aws-sdk/client-s3' + +const BACKUP_RETENTION_DAYS = 3 + +export default defineTask({ + meta: { + name: 'db:backup', + description: 'Dump database and upload to S3 daily', + }, + async run() { + const now = new Date() + const timestamp = now.toISOString().split('T')[0] // YYYY-MM-DD + const backupFileName = `backup-${timestamp}.sql.gz` + const localBackupPath = path.join('/tmp', backupFileName) + + // S3 Environment Variables + const s3Bucket = process.env.AWS_S3_BUCKET + const s3Region = process.env.AWS_S3_REGION + const s3AccessKey = process.env.AWS_S3_ACCESS_KEY + const s3SecretKey = process.env.AWS_S3_SECRET_ACCESS_KEY + + if (!s3Bucket || !s3Region || !s3AccessKey || !s3SecretKey) { + console.error('S3 environment variables are not fully configured. Skipping backup.') + return { result: 'Error: Missing S3 environment variables' } + } + + // Database Environment Variables + const postgresUrl = process.env.POSTGRES_URL + const dbHost = process.env.POSTGRES_HOST + const dbPort = process.env.POSTGRES_PORT + const dbUser = process.env.POSTGRES_USER + const dbPassword = process.env.POSTGRES_PASSWORD + const dbName = process.env.POSTGRES_DB + + let pgDumpBaseCommand: string + const pgDumpExecaOptions: { shell: true, env: Record } = { shell: true, env: {} } + + if (postgresUrl) { + // Escape single quotes in the URL for shell safety, though pg_dump expects a raw URL. + // The primary concern is the shell interpreting the quotes, not pg_dump itself. + const escapedPostgresUrl = postgresUrl.replace(/'/g, '\'\\\\\'\'') + pgDumpBaseCommand = `pg_dump --dbname='${escapedPostgresUrl}' --format=c` + // PGPASSWORD is not typically set when the password is in the connection string for pg_dump. + console.log('Using POSTGRES_URL for database connection.') + } + else if (dbHost && dbPort && dbUser && dbPassword && dbName) { + pgDumpBaseCommand = `pg_dump --host=${dbHost} --port=${dbPort} --username=${dbUser} --dbname=${dbName} --format=c --no-password` + pgDumpExecaOptions.env.PGPASSWORD = dbPassword + console.log('Using individual POSTGRES_HOST/USER/DB variables for database connection.') + } + else { + console.error('Database connection environment variables are not fully configured. Provide POSTGRES_URL or all of POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB. Skipping backup.') + return { result: 'Error: Missing database environment variables' } + } + + // pg_dump command will output to stdout, then piped to gzip + const fullBackupCommand = `${pgDumpBaseCommand} | gzip > ${localBackupPath}` + + try { + console.log(`Starting database backup to ${localBackupPath}...`) + // Execute the piped command using shell + await execa(fullBackupCommand, pgDumpExecaOptions) + console.log('Database dump and compression successful.') + + const fileContent = await fs.readFile(localBackupPath) + const s3Key = `backups/database/${backupFileName}` + + console.log(`Uploading backup to S3 bucket ${s3Bucket} with key ${s3Key}...`) + const s3Client = getS3Client() + + await s3Client.send(new PutObjectCommand({ + Bucket: s3Bucket, + Key: s3Key, + Body: fileContent, + ContentType: 'application/gzip', // Specify content type for gzipped file + })) + console.log('Backup uploaded to S3 successfully.') + + // Implement 3-day retention policy + console.log('Applying 3-day retention policy...') + const listCommand = new ListObjectsV2Command({ + Bucket: s3Bucket, + Prefix: 'backups/database/', + }) + const listedObjects = await s3Client.send(listCommand) + + if (listedObjects.Contents && listedObjects.Contents.length > 0) { + const cutoffDate = new Date(now) + cutoffDate.setDate(now.getDate() - BACKUP_RETENTION_DAYS) // Keep today's and 2 previous days' backups + + const objectsToDelete = listedObjects.Contents.filter((obj) => { + if (!obj.Key) + return false + const match = obj.Key.match(/backup-(\\d{4}-\\d{2}-\\d{2})\\.sql\\.gz$/) + if (match && match[1]) { + const backupDate = new Date(match[1]) + // Ensure comparison is date-only by setting hours to 0 + backupDate.setHours(0, 0, 0, 0) + const comparisonCutoff = new Date(cutoffDate) + comparisonCutoff.setHours(0, 0, 0, 0) + return backupDate < comparisonCutoff + } + return false + }) + + if (objectsToDelete.length > 0) { + const deleteParams = { + Bucket: s3Bucket, + Delete: { + Objects: objectsToDelete.map(obj => ({ Key: obj.Key })), + Quiet: false, + }, + } + const deleteResult = await s3Client.send(new DeleteObjectsCommand(deleteParams)) + if (deleteResult.Deleted && deleteResult.Deleted.length > 0) { + console.log(`Successfully deleted ${deleteResult.Deleted.length} old backup(s): ${deleteResult.Deleted.map(d => d.Key).join(', ')}`) + } + if (deleteResult.Errors && deleteResult.Errors.length > 0) { + deleteResult.Errors.forEach(err => console.error(`Error deleting S3 object ${err.Key}: ${err.Message}`)) + } + } + else { + console.log('No old backups found to delete.') + } + } + else { + console.log('No backups found in S3 to apply retention policy.') + } + + return { result: 'Success', backupPath: s3Key } + } + catch (error: any) { + console.error('Database backup or S3 upload failed:', error.message) + if (error.stderr) { + console.error('pg_dump stderr:', error.stderr) + } + if (error.stdout) { + console.error('pg_dump stdout:', error.stdout) + } + return { result: 'Error', error: error.message } + } + finally { + try { + await fs.unlink(localBackupPath) + console.log(`Cleaned up local backup file: ${localBackupPath}`) + } + catch (cleanupError: any) { + // Log if cleanup fails but don't let it mask the primary error + console.warn(`Failed to clean up local backup file ${localBackupPath}:`, cleanupError.message) + } + } + }, +}) diff --git a/server/tasks/email/test.ts b/server/tasks/email/test.ts new file mode 100644 index 00000000..0e13b1f0 --- /dev/null +++ b/server/tasks/email/test.ts @@ -0,0 +1,17 @@ +export default defineTask({ + meta: { + name: 'email:test', + description: 'Test email sender', + }, + async run() { + const { sendMail } = useNodeMailer() + + await sendMail({ + subject: 'Hehe test', + to: 'nguyenhuunguyeny.ny@gmail.com', + html: 'This is a test email', + }) + + return { result: 'Success' } + }, +}) diff --git a/server/types/logto.ts b/server/types/logto.ts new file mode 100644 index 00000000..91468247 --- /dev/null +++ b/server/types/logto.ts @@ -0,0 +1,12 @@ +import type { UserInfoResponse } from '@logto/nuxt' + +export interface LogtoUser extends UserInfoResponse { + custom_data: { + credit?: number + email?: boolean + desktop?: boolean + product_updates?: boolean + weekly_digest?: boolean + important_updates?: boolean + } +} diff --git a/server/types/models.ts b/server/types/models.ts new file mode 100644 index 00000000..208db994 --- /dev/null +++ b/server/types/models.ts @@ -0,0 +1,95 @@ +import type { InferSelectModel } from 'drizzle-orm' +import type { + creditHistoryTable, + deviceTable, + identityTable, + notificationTable, + orderTable, + paymentProviderTransactionTable, + paymentTable, + productTable, + referenceTable, + userTable, +} from '../db/schemas' + +/** + * Database model types derived from schema tables + */ +export type User = InferSelectModel +export type Identity = InferSelectModel +export type Notification = InferSelectModel +export type CreditHistory = InferSelectModel +export type Order = InferSelectModel +export type Payment = InferSelectModel +export type PaymentProviderTransaction = InferSelectModel +export type Product = InferSelectModel +export type Device = InferSelectModel +export type Reference = InferSelectModel + +/** + * Utility types for handling nullable values consistently + */ +export type Nullable = T | null + +/** + * Makes all properties in an object nullable + */ +export type NullableProps = { + [P in keyof T]: Nullable +} + +/** + * Makes all properties optional + */ +export type Optional = { + [P in keyof T]?: T[P] +} + +/** + * Combines properties being both optional and nullable + */ +export type OptionalNullable = Optional> + +/** + * Creates a user input type with specific included fields only + */ +export type UserInput = OptionalNullable< + Pick +> + +/** + * Type for basic filter pagination + */ +export interface PaginationOptions { + page?: number + limit?: number + sortBy?: string + sortAsc?: boolean + keyword?: string + keywordLower?: string + withCount?: boolean +} + +/** + * Type for API responses with pagination + */ +export interface PaginatedResponse { + data: T[] + total: number +} diff --git a/server/utils/array.ts b/server/utils/array.ts deleted file mode 100644 index 86aded6e..00000000 --- a/server/utils/array.ts +++ /dev/null @@ -1 +0,0 @@ -export { omit } from '@base/utils/array' diff --git a/server/utils/auth/scope.ts b/server/utils/auth/scope.ts index 0e961753..677e2125 100644 --- a/server/utils/auth/scope.ts +++ b/server/utils/auth/scope.ts @@ -1,5 +1,6 @@ import type { IncomingHttpHeaders } from 'node:http' import { createRemoteJWKSet, jwtVerify } from 'jose' +import { cleanDoubleSlashes } from 'ufo' function extractBearerTokenFromHeaders({ authorization }: IncomingHttpHeaders) { if (!authorization) { @@ -19,7 +20,7 @@ export async function getUserScopes() { const event = useEvent() // Generate a JWKS using jwks_uri obtained from the Logto server - const jwks = createRemoteJWKSet(new URL(`${process.env.LOGTO_ENDPOINT}/oidc/jwks`)) + const jwks = createRemoteJWKSet(new URL(cleanDoubleSlashes(`${process.env.LOGTO_ENDPOINT}/oidc/jwks`))) const token = extractBearerTokenFromHeaders(event.node.req.headers) @@ -29,7 +30,7 @@ export async function getUserScopes() { jwks, { // Expected issuer of the token, issued by the Logto server - issuer: `${process.env.LOGTO_ENDPOINT}/oidc`, + issuer: cleanDoubleSlashes(`${process.env.LOGTO_ENDPOINT}/oidc`), // Expected audience token, the resource indicator of the current API audience: config.public.apiBaseUrl, }, diff --git a/server/utils/db.ts b/server/utils/db.ts index c0c8877c..9c98987e 100644 --- a/server/utils/db.ts +++ b/server/utils/db.ts @@ -10,12 +10,12 @@ export const db = drizzle( process.env.POSTGRES_URL ? postgres(process.env.POSTGRES_URL) : postgres({ - host: process.env.POSTGRES_HOST!, - port: Number(process.env.POSTGRES_PORT), - user: process.env.POSTGRES_USER!, - password: process.env.POSTGRES_PASSWORD!, - database: process.env.POSTGRES_DB!, - }), + host: process.env.POSTGRES_HOST!, + port: Number(process.env.POSTGRES_PORT), + user: process.env.POSTGRES_USER!, + password: process.env.POSTGRES_PASSWORD!, + database: process.env.POSTGRES_DB!, + }), { schema, }, diff --git a/server/utils/error-message.ts b/server/utils/error-message.ts deleted file mode 100644 index 7e239642..00000000 --- a/server/utils/error-message.ts +++ /dev/null @@ -1,16 +0,0 @@ -export enum ErrorMessage { - UNAUTHORIZED = 'Unauthorized!', - CANNOT_FIND_ROLE = 'Cannot assign role and permissions to user!', - INTERNAL_SERVER_ERROR = 'Internal server error!', - INVALID_CREDENTIALS = 'Invalid signin credentials!', - CANNOT_CHECKOUT = 'Cannot create Stripe Checkout session!', - DONOT_HAVE_PERMISSION = 'You do not have permission to perform this action!', - INVALID_PARAMS = 'Invalid parameter: <%= key %>, receive value: <%= value %>', - STRIPE_NO_PRICE = 'No price found for this product!', - BAD_REQUEST = 'Bad request!', - EMAIL_NOT_VERIFIED = 'Email not verified!', - INVALID_VERIFICATION_URL = 'Invalid verification URL!', - EMAIL_ALREADY_VERIFIED = 'Email already verified!', - PASSWORD_MISMATCH = 'Password mismatch!', - INVALID_BODY = 'Invalid request body!', -} diff --git a/server/utils/error.ts b/server/utils/error.ts index 586d7e81..4e2c45ab 100644 --- a/server/utils/error.ts +++ b/server/utils/error.ts @@ -1,14 +1,27 @@ -import type { PostgresError } from 'postgres' -import { pick } from 'lodash-es' +export enum ErrorMessage { + UNAUTHORIZED = 'Unauthorized!', + CANNOT_FIND_ROLE = 'Cannot assign role and permissions to user!', + INTERNAL_SERVER_ERROR = 'Internal server error!', + INVALID_CREDENTIALS = 'Invalid signin credentials!', + CANNOT_CHECKOUT = 'Cannot create Stripe Checkout session!', + DONOT_HAVE_PERMISSION = 'You do not have permission to perform this action!', + INVALID_PARAMS = 'Invalid parameter: <%= key %>, receive value: <%= value %>', + STRIPE_NO_PRICE = 'No price found for this product!', + BAD_REQUEST = 'Bad request!', + EMAIL_NOT_VERIFIED = 'Email not verified!', + INVALID_VERIFICATION_URL = 'Invalid verification URL!', + EMAIL_ALREADY_VERIFIED = 'Email already verified!', + PASSWORD_MISMATCH = 'Password mismatch!', + INVALID_WEBHOOK_BODY = 'Invalid webhook body!', +} export function parseError(error: any) { if (error.name === 'PostgresError') { - const _error: PostgresError = error - return createError({ statusCode: 400, - statusMessage: _error.message, - data: pick(_error, ['code', 'table_name', 'constraint_name', 'detail']), + statusMessage: ErrorMessage.BAD_REQUEST, + data: error, + stack: '', }) } @@ -16,5 +29,6 @@ export function parseError(error: any) { statusCode: error.statusCode || 500, statusMessage: error.message, data: error, + stack: '', }) } diff --git a/server/utils/firebase.ts b/server/utils/firebase.ts index f999cc70..85e9732c 100644 --- a/server/utils/firebase.ts +++ b/server/utils/firebase.ts @@ -1,17 +1,39 @@ -import admin from 'firebase-admin' +import firebaseAdmin from 'firebase-admin' export function getFirebaseServiceAccount() { return { + type: 'service_account', project_id: process.env.FIREBASE_PROJECT_ID, private_key_id: process.env.FIREBASE_PRIVATE_KEY_ID, private_key: process.env.FIREBASE_PRIVATE_KEY, - client_email: process.env.FIREBASE_CLIENT_EMAIL, + client_email: `firebase-adminsdk-fmxbs@${process.env.FIREBASE_PROJECT_ID}.iam.gserviceaccount.com`, client_id: process.env.FIREBASE_CLIENT_ID, - client_x509_cert_url: process.env.FIREBASE_CLIENT_X509_CERT_URL, - type: 'service_account', auth_uri: 'https://accounts.google.com/o/oauth2/auth', token_uri: 'https://oauth2.googleapis.com/token', auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs', + client_x509_cert_url: `https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fmxbs%40${process.env.FIREBASE_PROJECT_ID}.iam.gserviceaccount.com`, universe_domain: 'googleapis.com', - } as admin.ServiceAccount + } +} + +export async function sendFirebaseCloudMessage(userId: string, title: string, body: string) { + const service = getFirebaseServiceAccount() + + if (firebaseAdmin.apps.length === 0) { + firebaseAdmin.initializeApp({ + credential: firebaseAdmin.credential.cert(service as firebaseAdmin.ServiceAccount), + }) + } + + const { getDeviceTokens } = useDeviceToken() + + const deviceTokens = await getDeviceTokens(userId) + + return firebaseAdmin.messaging().sendEachForMulticast({ + tokens: deviceTokens.map(device => device.token_device).filter(Boolean) as string[], + notification: { + title, + body, + }, + }) } diff --git a/server/utils/index.ts b/server/utils/index.ts index a921386f..95d991e8 100644 --- a/server/utils/index.ts +++ b/server/utils/index.ts @@ -1,6 +1,8 @@ -export * from './stripe' +export * from './auth' + +export * from './payment' -export * from './array' +export * from './stripe' export * from './db' @@ -19,5 +21,3 @@ export * from './s3' export * from './storage' export * from './url' - -export * from './payment' diff --git a/server/utils/logger.ts b/server/utils/logger.ts new file mode 100644 index 00000000..ecc80162 --- /dev/null +++ b/server/utils/logger.ts @@ -0,0 +1,291 @@ +import { promises as fs, mkdirSync } from 'node:fs' +import path, { join } from 'node:path' +import type { H3Event } from 'h3' +import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3' +import winston from 'winston' +import DailyRotateFile from 'winston-daily-rotate-file' + +export type LogLevel = 'log' | 'info' | 'warn' | 'error' + +const customToWinstonLevel: Record = { + error: 'error', + warn: 'warn', + info: 'info', + log: 'verbose', +} + +function winstonToCustomLevel(winstonLevel: string): LogLevel | undefined { + if (winstonLevel === 'verbose') { + return 'log' + } + if (['error', 'warn', 'info'].includes(winstonLevel)) { + return winstonLevel as LogLevel + } + return undefined +} + +const isDev = process.env.NODE_ENV === 'development' + +export class Logger { + private logger: winston.Logger + private s3Bucket: string | null = null + private s3Client?: S3Client + private enabledLogLevels: Set | null = null + private logsDir: string + private dailyRotateFileTransport: DailyRotateFile + + constructor(logsDirOpt?: string) { + this.logsDir = logsDirOpt || join(process.cwd(), 'logs') + this.ensureLogDirectoryExistsSync() + this.initS3Config() + this.initLogLevels() + + const winstonLogLevel = this.determineWinstonLogLevel() + + const levelFilter = winston.format((info) => { + if (this.enabledLogLevels === null) { + return info + } + const customLevel = winstonToCustomLevel(info.level) + return customLevel && this.enabledLogLevels.has(customLevel) ? info : false + }) + + this.dailyRotateFileTransport = new DailyRotateFile({ + level: winstonLogLevel, + dirname: this.logsDir, + filename: '%DATE%.log', + zippedArchive: false, + frequency: isDev ? '1m' : '15m', + maxFiles: '2d', + datePattern: 'YYYY-MM-DD-HH-mm', + format: winston.format.combine( + winston.format.timestamp(), + winston.format.errors({ stack: true }), + levelFilter(), + winston.format.json(), + ), + }) + + this.dailyRotateFileTransport.on('archive', (archivedPath: string) => { + if (this.s3Bucket && this.s3Client) { + this.uploadToS3(archivedPath).catch(err => console.error('S3 upload failed from archive event:', err)) + } + }) + + this.dailyRotateFileTransport.on('error', (error: Error) => { + console.error('Winston DailyRotateFile Transport Error:', error) + }) + + this.logger = winston.createLogger({ + level: winstonLogLevel, + transports: [ + new winston.transports.Console({ + format: winston.format.combine( + winston.format.colorize(), + winston.format.simple(), + levelFilter(), + ), + silent: this.enabledLogLevels !== null && this.enabledLogLevels.size === 0, + }), + this.dailyRotateFileTransport, + ], + }) + + if (this.enabledLogLevels !== null && this.enabledLogLevels.size === 0) { + console.info('Logger: All logging is disabled (LOG_LEVEL is empty array or all levels filtered out)') + } + } + + private initLogLevels() { + const logLevelEnv = process.env.LOG_LEVEL + + if (logLevelEnv === undefined) { + this.enabledLogLevels = null + console.info('Logger: LOG_LEVEL is undefined, all log levels enabled.') + } + else { + try { + const parsedLevels = JSON.parse(logLevelEnv) + if (Array.isArray(parsedLevels)) { + if (parsedLevels.length === 0) { + this.enabledLogLevels = new Set() + console.info('Logger: LOG_LEVEL is an empty array, file logging disabled.') + } + else { + const validLevels = parsedLevels.filter( + (level): level is LogLevel => Object.keys(customToWinstonLevel).includes(level), + ) + this.enabledLogLevels = new Set(validLevels) + console.info(`Logger: Enabled log levels: ${Array.from(this.enabledLogLevels).join(', ')}`) + } + } + else { + this.enabledLogLevels = null + console.warn('Logger: Invalid LOG_LEVEL format, all log levels enabled.') + } + } + catch (error) { + this.enabledLogLevels = null + console.warn(`Logger: Failed to parse LOG_LEVEL, all log levels enabled: ${error}`) + } + } + } + + private determineWinstonLogLevel(): string { + if (this.enabledLogLevels === null) { + return 'silly' + } + if (this.enabledLogLevels.size === 0) { + return 'error' + } + + const winstonLevelsHierarchy: { [key: string]: number } = { error: 0, warn: 1, info: 2, verbose: 3, debug: 4, silly: 5 } + let maxNumericLevel = -1 + + this.enabledLogLevels.forEach((level) => { + const winstonLevel = customToWinstonLevel[level] + if (winstonLevel && winstonLevelsHierarchy[winstonLevel] !== undefined) { + maxNumericLevel = Math.max(maxNumericLevel, winstonLevelsHierarchy[winstonLevel]) + } + }) + + if (maxNumericLevel === -1) { + return 'error' + } + + for (const levelStr in winstonLevelsHierarchy) { + if (winstonLevelsHierarchy[levelStr] === maxNumericLevel) { + return levelStr + } + } + return 'silly' + } + + private initS3Config() { + const bucket = process.env.AWS_LOGGER_S3_BUCKET + const region = process.env.AWS_LOGGER_S3_REGION || process.env.AWS_S3_REGION + const accessKeyId = process.env.AWS_S3_ACCESS_KEY + const secretAccessKey = process.env.AWS_S3_SECRET_ACCESS_KEY + + if (bucket && region && accessKeyId && secretAccessKey) { + this.s3Bucket = bucket + this.s3Client = new S3Client({ + region, + credentials: { accessKeyId, secretAccessKey }, + }) + console.info(`Logger: S3 uploads enabled to bucket ${bucket} in region ${region}.`) + } + else { + console.warn('Logger: S3 upload disabled due to missing AWS configuration (bucket, region, or credentials).') + } + } + + private ensureLogDirectoryExistsSync() { + try { + mkdirSync(this.logsDir, { recursive: true }) + } + catch (error) { + console.error(`Logger: Error creating logs directory '${this.logsDir}':`, error) + } + } + + private async uploadToS3(filePath: string) { + if (!this.s3Bucket || !this.s3Client) { + console.warn(`Logger: S3 upload skipped for ${filePath}, S3 not configured.`) + return + } + + try { + const fileStats = await fs.stat(filePath) + if (fileStats.size === 0) { + console.info(`Logger: Skipping empty archived log file: ${filePath}`) + await fs.unlink(filePath) + return + } + + const fileContent = await fs.readFile(filePath) + const fileName = path.basename(filePath) + + const dateMatch = fileName.match(/(\d{4}-\d{2}-\d{2})/) + let s3KeyPrefix = 'logs/unknown-date/' + if (dateMatch && dateMatch[1]) { + const [year, month, day] = dateMatch[1].split('-') + s3KeyPrefix = `logs/${year}/${month}/${day}/` + } + + const s3Key = `${s3KeyPrefix}${fileName}` + + await this.s3Client.send(new PutObjectCommand({ + Bucket: this.s3Bucket, + Key: s3Key, + Body: fileContent, + ContentType: 'text/plain', // Changed from application/gzip + })) + + console.info(`Logger: Successfully uploaded log file to S3: ${s3Key}`) + await fs.unlink(filePath) + console.info(`Logger: Deleted local archived log file: ${filePath}`) + } + catch (error) { + console.error(`Logger: Error uploading log file ${filePath} to S3:`, error) + } + } + + public log(message: any, meta?: Record) { + this.logger.verbose(message, meta) + } + + public info(message: any, meta?: Record) { + this.logger.info(message, meta) + } + + public warn(message: any, meta?: Record) { + this.logger.warn(message, meta) + } + + public error(message: any, meta?: Record) { + this.logger.error(message, meta) + } + + public createRequestLogger() { + return async (event: H3Event, meta?: Record) => { + const isInfoEnabled = this.enabledLogLevels === null || this.enabledLogLevels.has('info') + if (!isInfoEnabled || (this.enabledLogLevels !== null && this.enabledLogLevels.size === 0)) { + return + } + + const headers = event.headers + const headerEntries = Array.from(headers.entries()) + const filteredHeaders = headerEntries.filter( + ([key]) => !key.toLowerCase().includes('authorization') && !key.toLowerCase().includes('cookie'), + ) + + let body = null + try { + if (['POST', 'PUT', 'PATCH'].includes(event.method)) { + body = await readBody(event).catch(() => null) + } + } + catch (error) { + this.warn('Logger: Failed to read request body for logging:', { error }) + } + + const requestMeta = { + method: event.method, + url: event.path, + params: event.context.params, + query: getQuery(event), + headers: Object.fromEntries(filteredHeaders as Array<[string, string]>), + ip: headers.get('x-forwarded-for') || headers.get('x-real-ip') || 'unknown', + body, + ...meta, + } + + this.info(`Request: ${event.method} ${event.path}`, requestMeta) + } + } +} + +const logger = new Logger() + +export { logger } diff --git a/server/utils/logto.ts b/server/utils/logto.ts index 7a864226..81bcbf10 100644 --- a/server/utils/logto.ts +++ b/server/utils/logto.ts @@ -1,5 +1,6 @@ import type LogtoClient from '@logto/node' -import type { UserInfoResponse } from '@logto/node' +import type { LogtoUser } from '@base/server/types/logto' +import { cleanDoubleSlashes } from 'ufo' type LogtoAccountCenterFieldStatus = 'Off' | 'Edit' | 'ReadOnly' @@ -23,7 +24,7 @@ interface LogtoAccountCenterSettings { export function useLogtoUser() { const event = useEvent() - return (event.context?.logtoUser as UserInfoResponse) || null + return (event.context?.logtoUser as LogtoUser) || null } export function useLogtoClient() { @@ -32,9 +33,21 @@ export function useLogtoClient() { return (event.context?.logtoClient as LogtoClient) || null } +export async function getLogtoUserById(userId: string) { + const { access_token: accessToken } = await fetchM2MAccessToken() + + const response = await $fetch>(cleanDoubleSlashes(`${process.env.LOGTO_ENDPOINT!}/api/users/${userId}`), { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + + return response +} + export async function getLogtoUserCustomData(userId: string) { const { access_token: accessToken } = await fetchM2MAccessToken() - const response = await $fetch>(`${process.env.LOGTO_ENDPOINT!}/api/users/${userId}/custom-data`, { + const response = await $fetch>(cleanDoubleSlashes(`${process.env.LOGTO_ENDPOINT!}/api/users/${userId}/custom-data`), { headers: { Authorization: `Bearer ${accessToken}`, }, @@ -45,7 +58,7 @@ export async function getLogtoUserCustomData(userId: string) { export async function updateLogtoUserCustomData(userId: string, customData: Record) { const { access_token: accessToken } = await fetchM2MAccessToken() - const response = await $fetch(`${process.env.LOGTO_ENDPOINT!}/api/users/${userId}/custom-data`, { + const response = await $fetch(cleanDoubleSlashes(`${process.env.LOGTO_ENDPOINT!}/api/users/${userId}/custom-data`), { method: 'PATCH', headers: { 'Content-Type': 'application/json', @@ -60,7 +73,7 @@ export async function updateLogtoUserCustomData(userId: string, customData: Reco } export async function fetchM2MAccessToken() { - const response = await $fetch<{ access_token: string }>(`${process.env.LOGTO_ENDPOINT!}/oidc/token`, { + const response = await $fetch<{ access_token: string }>(cleanDoubleSlashes(`${process.env.LOGTO_ENDPOINT!}/oidc/token`), { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', @@ -82,7 +95,7 @@ export async function fetchM2MAccessToken() { export async function enableAccountCenter() { const { access_token: accessToken } = await fetchM2MAccessToken() - const accountCenterSettings = await $fetch(`${process.env.LOGTO_ENDPOINT!}/api/account-center`, { + const accountCenterSettings = await $fetch(cleanDoubleSlashes(`${process.env.LOGTO_ENDPOINT!}/api/account-center`), { headers: { Authorization: `Bearer ${accessToken}`, }, @@ -91,7 +104,7 @@ export async function enableAccountCenter() { if (accountCenterSettings.enabled) return - await $fetch(`${process.env.LOGTO_ENDPOINT!}/api/account-center`, { + await $fetch(cleanDoubleSlashes(`${process.env.LOGTO_ENDPOINT!}/api/account-center`), { method: 'PATCH', body: { enabled: true, diff --git a/server/utils/notification.ts b/server/utils/notification.ts index 989d1cdc..a8d22904 100644 --- a/server/utils/notification.ts +++ b/server/utils/notification.ts @@ -1,5 +1,4 @@ -import admin from 'firebase-admin' -import { useUserDeviceCrud } from '@base/server/composables/useUserDeviceCrud' +import firebaseAdmin from 'firebase-admin' interface NotificationBody { user_id: string @@ -7,24 +6,26 @@ interface NotificationBody { body: string link: string } + export async function pushNotification(param: NotificationBody) { if (!param.user_id) return const service = getFirebaseServiceAccount() - if (admin.apps.length === 0) { - admin.initializeApp({ - credential: admin.credential.cert(service), + if (firebaseAdmin.apps.length === 0) { + firebaseAdmin.initializeApp({ + credential: firebaseAdmin.credential.cert(service as firebaseAdmin.ServiceAccount), }) } - const { getUserDeviceAllTokens } = useUserDeviceCrud({ user_id: param.user_id }) - const response = await getUserDeviceAllTokens({} as ParsedFilterQuery) + const { getDeviceTokens } = useDeviceToken() + + const response = await getDeviceTokens(param.user_id) - if (response && response.total === 0) + if (response && response.length === 0) return - const tokens = response.data!.map((item: any) => item.token_device) + const tokens = response!.map((item: any) => item.token_device) const body = { tokens, notification: { @@ -38,7 +39,9 @@ export async function pushNotification(param: NotificationBody) { }, } - const res = await admin.messaging().sendEachForMulticast(body) - console.log('push:', res) + const res = await firebaseAdmin.messaging().sendEachForMulticast(body) + + logger.log('Notification pushed:', res) + return res } diff --git a/server/utils/options.ts b/server/utils/options.ts index 60200c93..862f58c5 100644 --- a/server/utils/options.ts +++ b/server/utils/options.ts @@ -1,6 +1,6 @@ import type { H3Event } from 'h3' import { z } from 'zod' -import type { UserInfoResponse } from '@logto/node' +import type { User } from '../types/models' import { useLogtoUser } from '#imports' interface RouteOptions { @@ -17,7 +17,7 @@ export async function defineEventOptions< UseAuthU extends boolean, ParamsT extends string[], >(event: H3Event, options?: RouteOptions>) { - type SessionType = ConditionalType + type SessionType = ConditionalType type Result = { [K in TupleType[number]]: string @@ -37,7 +37,16 @@ export async function defineEventOptions< }) } - result.session = user as SessionType + const currentUser = await useUser().getUserById(user.sub) + + if (!currentUser) { + throw createError({ + statusCode: 401, + statusMessage: ErrorMessage.UNAUTHORIZED, + }) + } + + result.session = currentUser as SessionType } if (options?.scopes?.length) { diff --git a/server/utils/payment/credit/index.ts b/server/utils/payment/credit/index.ts new file mode 100644 index 00000000..8059a7f4 --- /dev/null +++ b/server/utils/payment/credit/index.ts @@ -0,0 +1,37 @@ +import { CreditHistoryType } from '@base/server/db/schemas' + +export async function addCreditToUser(userId: string, amount: number) { + const { getUserCreditById } = useUser() + + const userCredit = await getUserCreditById(userId) + + const newAmount = userCredit + amount + + const { updateCreditHistory, updateUserCredit } = useCredit() + + await updateCreditHistory(CreditHistoryType.TOPUP, amount, userId) + + await updateUserCredit(userId, newAmount) + + await useNitroApp().hooks.callHook('credit:change', { userId, amount: newAmount }) + + return { success: true } +} + +export async function subtractCreditFromUser(userId: string, amount: number) { + const { getUserCreditById } = useUser() + + const userCredit = await getUserCreditById(userId) + + const newAmount = userCredit - amount + + const { updateCreditHistory, updateUserCredit } = useCredit() + + await updateCreditHistory(CreditHistoryType.SPEND, amount, userId) + + await updateUserCredit(userId, newAmount) + + await useNitroApp().hooks.callHook('credit:change', { userId, amount: newAmount }) + + return { success: true } +} diff --git a/server/utils/payment/index.ts b/server/utils/payment/index.ts index 099638b5..0b743dbe 100644 --- a/server/utils/payment/index.ts +++ b/server/utils/payment/index.ts @@ -1 +1,3 @@ +export * from './credit/index' + export * from './vn/index' diff --git a/server/utils/payment/vn/index.ts b/server/utils/payment/vn/index.ts index 6f09764b..45734ab7 100644 --- a/server/utils/payment/vn/index.ts +++ b/server/utils/payment/vn/index.ts @@ -1,48 +1,41 @@ -import { PaymentStatus, creditPackageTable, paymentProviderTransactionTable, userOrderTable, userPaymentTable } from '@base/server/db/schemas' -import type { UserInfoResponse } from '@logto/nuxt' -import { eq } from 'drizzle-orm' -import { createPayOSCheckout } from './payos' -import { createVNPayCheckout } from './vnpay' +import type { User } from '@base/server/types/models' +import { customAlphabet } from 'nanoid' +import { createSePayCheckout } from './sepay' export * from './payos' -export * from './vnpay' - export async function createPaymentCheckout( - provider: 'payos' | 'vnpay', + provider: 'payos' | 'vnpay' | 'sepay', payload: { clientIP?: string productIdentifier: string - user: UserInfoResponse + user: User }, ) { - if (!payload.productIdentifier || !payload.user || !payload.user.sub) { + if (!payload.productIdentifier || !payload.user || !payload.user.id) { throw createError({ statusCode: 400, - statusMessage: ErrorMessage.INVALID_BODY, + statusMessage: ErrorMessage.INVALID_WEBHOOK_BODY, }) } const [productType, productId] = payload.productIdentifier.split(':') - let productInfo: any + let productInfo: { id: string, price: number, amount: number, price_discount: number | null } | undefined + + const { createOrder, createPayment, createProviderTransaction } = usePayment() + const { getProductByProductId } = useProduct() + const { getReferenceByCode } = useReference() switch (productType) { case 'credit': - productInfo = await db.query.creditPackageTable.findFirst({ - where: eq(creditPackageTable.id, productId), - columns: { - id: true, - price: true, - amount: true, - }, - }) + productInfo = await getProductByProductId(productId) break default: throw createError({ statusCode: 400, - statusMessage: ErrorMessage.INVALID_BODY, + statusMessage: ErrorMessage.INVALID_WEBHOOK_BODY, }) } @@ -53,61 +46,44 @@ export async function createPaymentCheckout( }) } - // TODO: what if the user has an existing order? - const date = new Date() - const { - userPayment, - paymentProviderTransaction, - } = await db.transaction(async (db) => { - const userOrder = (await db.insert(userOrderTable).values({ - user_id: payload.user.sub, - }).returning())[0] - - const userPayment = (await db.insert(userPaymentTable).values({ - amount: productInfo.price, - status: PaymentStatus.PENDING, - user_id: payload.user.sub, - order_id: userOrder.id, - created_at: date, - }).returning())[0] - - const paymentProviderTransaction = (await db.insert(paymentProviderTransactionTable).values({ - provider, - provider_transaction_id: date.getTime().toString(), - provider_transaction_status: PaymentStatus.PENDING, - provider_transaction_info: `${productType}:${productInfo.amount}`, - payment_id: userPayment.id, - user_id: payload.user.sub, - created_at: date, - }).returning())[0] - - return { - userPayment, - paymentProviderTransaction, - } - }) + const { getUserBestPrice } = useReference() + + const referCode = getCookie(useEvent(), REFERENCE_CODE_COOKIE_NAME) + + const price = await getUserBestPrice(payload.user.id, productInfo.price, productInfo.price_discount, referCode) + + var reference = await getReferenceByCode(referCode || '') + const userOrder = await createOrder(productId, payload.user.id, reference?.id) + + const userPayment = await createPayment( + userOrder.id, + payload.user.id, + price, + ) + + // exclude underscore _ + const orderCode = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 16)() + + await createProviderTransaction( + userPayment.id, + payload.user.id, + orderCode, + provider, + productType, + productInfo, + ) switch (provider) { - case 'payos': - return await createPayOSCheckout({ - date, - amount: Number.parseInt(userPayment.amount), - buyerEmail: payload.user.email as string, - buyerPhone: payload.user.phone_number as string, - paymentProviderTransaction, - }) - case 'vnpay': - return createVNPayCheckout({ - date, - clientIP: payload.clientIP as string, - userPayment, - paymentProviderTransaction, + case 'sepay': + return await createSePayCheckout({ + orderCode, + amount: userPayment.amount, }) default: throw createError({ statusCode: 400, - statusMessage: ErrorMessage.INVALID_BODY, + statusMessage: ErrorMessage.INVALID_WEBHOOK_BODY, }) } } diff --git a/server/utils/payment/vn/payos.ts b/server/utils/payment/vn/payos.ts index 56ccaf2a..c6a159f1 100644 --- a/server/utils/payment/vn/payos.ts +++ b/server/utils/payment/vn/payos.ts @@ -2,34 +2,39 @@ import type { paymentProviderTransactionTable } from '@base/server/db/schemas' import PayOS from '@payos/node' -interface payOSCheckoutProps { - date: Date +interface PayOSCheckoutProps { + orderCode: number amount: number buyerEmail?: string buyerPhone?: string paymentProviderTransaction: typeof paymentProviderTransactionTable.$inferSelect } -export const payOSAdmin = new PayOS( - process.env.PAYOS_CLIENT_ID!, - process.env.PAYOS_API_KEY!, - process.env.PAYOS_CHECKSUM_KEY!, -) +export function getPayOSAdmin() { + const config = useRuntimeConfig() + + return new PayOS( + config.payos.clientId, + config.payos.apiKey, + config.payos.checksumKey, + ) +} export async function createPayOSCheckout({ - date, + orderCode, amount, buyerEmail, buyerPhone, paymentProviderTransaction, -}: payOSCheckoutProps) { - const runtimeConfig = useRuntimeConfig() - const { checkoutUrl } = await payOSAdmin.createPaymentLink({ - orderCode: date.getTime(), +}: PayOSCheckoutProps) { + const config = useRuntimeConfig() + + const { checkoutUrl } = await getPayOSAdmin().createPaymentLink({ + orderCode, amount, description: paymentProviderTransaction.provider_transaction_info, - cancelUrl: `${runtimeConfig.public.appBaseUrl}/api/payments/payos/cancel`, - returnUrl: `${runtimeConfig.public.appBaseUrl}/api/payments/payos/callback`, + cancelUrl: config.payos.cancelUrl || `${config.public.appBaseUrl}/app/settings/billing`, + returnUrl: config.payos.returnUrl || `${config.public.appBaseUrl}/app/settings/billing`, buyerEmail, buyerPhone, }) diff --git a/server/utils/payment/vn/sepay.ts b/server/utils/payment/vn/sepay.ts new file mode 100644 index 00000000..a28e8e61 --- /dev/null +++ b/server/utils/payment/vn/sepay.ts @@ -0,0 +1,29 @@ +import { withQuery } from 'ufo' + +interface SePayCheckoutProps { + orderCode: string + amount: number +} + +export async function createSePayCheckout({ + orderCode, + amount, +}: SePayCheckoutProps) { + const prefix = process.env.SEPAY_TRANSACTION_PREFIX || 'SP' + + if (prefix.length !== 2) { + throw createError({ + statusCode: 400, + statusMessage: 'Transaction prefix must be exactly 2 characters long.', + }) + } + + return withQuery('https://qr.sepay.vn/img', { + acc: process.env.SEPAY_BANK_NUMBER, + bank: process.env.SEPAY_BANK_NAME, + amount, + des: [prefix, orderCode].join(''), + template: 'compact', + download: false, + }) +} diff --git a/server/utils/payment/vn/vnpay.ts b/server/utils/payment/vn/vnpay.ts deleted file mode 100644 index 442502e9..00000000 --- a/server/utils/payment/vn/vnpay.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ProductCode, VNPay, VnpLocale, ignoreLogger } from 'vnpay' -import { format } from 'date-fns' -import type { paymentProviderTransactionTable, userPaymentTable } from '@base/server/db/schemas' - -interface VNPayCheckoutProps { - date: Date - clientIP: string - userPayment: typeof userPaymentTable.$inferSelect - paymentProviderTransaction: typeof paymentProviderTransactionTable.$inferSelect -} - -export const vnpayAdmin = new VNPay({ - tmnCode: process.env.VNP_TMNCODE!, - secureSecret: process.env.VNP_HASHSECRET!, - testMode: !process.env.DISABLE_TEST_MODE, - loggerFn: ignoreLogger, -}) - -export function createVNPayCheckout({ - date, - clientIP, - userPayment, - paymentProviderTransaction, -}: VNPayCheckoutProps) { - const runtimeConfig = useRuntimeConfig() - const paymentUrl = vnpayAdmin.buildPaymentUrl({ - vnp_Amount: Number(userPayment.amount), - vnp_CreateDate: Number(format(date, 'yyyyMMddHHmmss')), - vnp_IpAddr: clientIP || '127.0.0.1', // TODO: get real IP - vnp_Locale: VnpLocale.VN, - vnp_OrderInfo: paymentProviderTransaction.provider_transaction_info, - vnp_OrderType: ProductCode.Other, - vnp_ReturnUrl: `${runtimeConfig.public.appBaseUrl}/api/payments/vnpay/callback`, - vnp_TxnRef: userPayment.id, - }) - - return paymentUrl -} diff --git a/server/utils/storage.ts b/server/utils/storage.ts index 5cbafbde..27afe938 100644 --- a/server/utils/storage.ts +++ b/server/utils/storage.ts @@ -1,23 +1,11 @@ -export function getStorageSessionKey(providerAccountId: string) { - return `session:${providerAccountId}` -} +import type { TransactionOptions } from 'unstorage' export function getStorageStripeKey(identifier: string) { return `stripe:${identifier}` } -function getStorage() { - const config = useRuntimeConfig() - - return (config.redis.host && config.redis.port && config.redis.password) - ? useStorage('redis') - : config.mongodb.connectionString - ? useStorage('mongodb') - : useStorage() -} - -export async function tryWithCache(key: string, getter: () => Promise) { - const storage = getStorage() +export async function tryWithCache(key: string, getter: () => Promise, options?: TransactionOptions) { + const storage = useStorage('redis') const cachedResult = await storage.getItem(key) @@ -27,13 +15,13 @@ export async function tryWithCache(key: string, getter: () => Promise