├── !Dockerfile ├── .eslintrc.json ├── .example.env.local ├── .gitignore ├── Dockerfile ├── Dockerfile.dev ├── LICENSE ├── README.md ├── docker-compose.dev.yml ├── docker-compose.yml ├── fluent-bit └── fluent-bit.conf ├── kong.yml ├── messages ├── ar.json ├── ca.json ├── cs.json ├── de.json ├── el.json ├── en.json ├── es.json ├── fr.json ├── gl.json ├── it.json ├── ja.json ├── kab.json ├── pt.json └── sv.json ├── next-auth.d.ts ├── next.config.js ├── nginx └── nginx.conf ├── package.json ├── postcss.config.js ├── public ├── boats │ ├── boat-1.svg │ ├── boat-2.svg │ ├── boat-3.svg │ ├── boat-4.svg │ ├── boat-5.svg │ ├── boat-6.svg │ ├── boat-7.svg │ ├── boat-8.svg │ └── boat-9.svg ├── client-metadata.json ├── compass │ ├── loader-compass.svg │ └── needle.svg ├── file.svg ├── globe.svg ├── logo-cnrs-blanc.svg ├── logo-cnrs-bleu.svg ├── logo.png ├── logo │ ├── logo-openport-blanc.svg │ └── logo-openport-rose.svg ├── logos │ ├── logo-openport-blanc.svg │ └── logo-openport-rose.svg ├── logoxHQX │ ├── HQX-blanc-FR.svg │ ├── HQX-pink-UK.svg │ ├── HQX-rose-FR.svg │ └── HQX-white-UK.svg ├── newSVG │ ├── BS.svg │ ├── HQX-badge.svg │ ├── X.svg │ ├── chainon.svg │ ├── close.svg │ ├── masto.svg │ ├── notif.svg │ ├── steps-Init.svg │ ├── steps-OK.svg │ ├── steps-partiel.svg │ ├── success-badge.svg │ ├── uil_arrow-down.svg │ ├── uil_arrow-growth.svg │ ├── upload.svg │ └── video.svg ├── next.svg ├── progress │ ├── progress-0.svg │ ├── progress-100.svg │ ├── progress-25.svg │ ├── progress-33.svg │ ├── progress-50.svg │ ├── progress-66.svg │ └── progress-75.svg ├── sea-wave.svg ├── sea.svg ├── v2 │ ├── HQX-badge.svg │ ├── Pause.svg │ ├── badge-success-1.png │ ├── badge-success-1.svg │ ├── badge-success-2-OFF.svg │ ├── badge-success-2.svg │ ├── chainon.svg │ ├── check.svg │ ├── close.svg │ ├── play.svg │ ├── statut=BS-defaut.svg │ ├── statut=BS-hover.svg │ ├── statut=Masto-Defaut.svg │ ├── statut=Masto-hover.svg │ ├── success-badge.svg │ ├── uil_arrow-down.svg │ └── uil_arrow-growth.svg ├── vercel.svg └── window.svg ├── python_worker ├── Dockerfile ├── dist │ ├── PythonProcessor.js │ └── index.js ├── messages │ ├── de.json │ ├── en.json │ ├── es.json │ ├── fr.json │ ├── it.json │ ├── pt.json │ └── sv.json ├── package-lock.json ├── package.json ├── requirements.txt ├── sendRecoNewsletter.py ├── src │ ├── PythonProcessor.ts │ ├── index.ts │ └── log_utils.ts ├── start-py-workers.sh ├── testDm.py ├── testDm_bluesky.py ├── testDm_mastodon.py └── tsconfig.json ├── src ├── app │ ├── [locale] │ │ ├── auth │ │ │ ├── error │ │ │ │ └── page.tsx │ │ │ └── signin │ │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── page.tsx │ │ ├── privacy_policy │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── reconnect │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── settings │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ └── upload │ │ │ ├── large-files │ │ │ └── page.tsx │ │ │ └── page.tsx │ ├── _components │ │ ├── AccountToMigrate.tsx │ │ ├── AppLayout.tsx │ │ ├── AutomaticReconnexion.tsx │ │ ├── BlueSkyLogin.tsx │ │ ├── BlueSkyLoginButton.tsx │ │ ├── BlueSkyPreviewModal.tsx │ │ ├── Boat.tsx │ │ ├── ConnectedAccounts.tsx │ │ ├── ConnectedServicesInfo.tsx │ │ ├── ConsentModal.tsx │ │ ├── DashboardLoginButtons.tsx │ │ ├── DashboardSea.tsx │ │ ├── ErrorModal.tsx │ │ ├── Footer.tsx │ │ ├── Header.tsx │ │ ├── LaunchReconnection.tsx │ │ ├── LoadingIndicator.tsx │ │ ├── LoginButtons.tsx │ │ ├── LoginSea.tsx │ │ ├── ManualReconnexion.tsx │ │ ├── MastodonLoginButton.tsx │ │ ├── MatchedBlueSkyProfiles.tsx │ │ ├── MigrateSea.tsx │ │ ├── MigrateStats.tsx │ │ ├── MigrationComplete.tsx │ │ ├── MotionWrapper.tsx │ │ ├── NewsLetterConsentsUpdate.tsx │ │ ├── NewsLetterFirstSeen.tsx │ │ ├── NewsletterRequest.tsx │ │ ├── PartageButton.tsx │ │ ├── ProfileCard.tsx │ │ ├── ProgressSteps.tsx │ │ ├── ReconnexionModaleResults.tsx │ │ ├── ReconnexionOptions.tsx │ │ ├── RefreshTokenModale.tsx │ │ ├── RequestNewsLetterDM.tsx │ │ ├── SettingsOptions.tsx │ │ ├── StatsReconnexion.tsx │ │ ├── SuccessAutomaticReconnexion.tsx │ │ ├── SuccessModal.tsx │ │ ├── SupportModale.tsx │ │ ├── TwitterLoginButton.tsx │ │ ├── TwitterRateLimit.tsx │ │ ├── UploadButton.tsx │ │ ├── UploadResults.tsx │ │ ├── dashboard │ │ │ ├── NewsletterSection.tsx │ │ │ ├── OnboardingSection.tsx │ │ │ └── TutorialSection.tsx │ │ ├── reconnect │ │ │ ├── ReconnectContainer.tsx │ │ │ ├── ReconnectOptionsSections.tsx │ │ │ ├── ReconnectStatusSection.tsx │ │ │ └── states │ │ │ │ ├── AutomaticReconnectionState.tsx │ │ │ │ ├── LoadingState.tsx │ │ │ │ ├── ManualReconnectionState.tsx │ │ │ │ ├── MissingTokenState.tsx │ │ │ │ ├── NoConnectedServicesState.tsx │ │ │ │ ├── PartialConnectedServicesState.tsx │ │ │ │ ├── ReconnectionCompleteState.tsx │ │ │ │ └── ShowOptionsState.tsx │ │ ├── settings │ │ │ ├── PersonalizedSupportFlowSection.tsx │ │ │ └── SwitchSettingsSection.tsx │ │ └── ui │ │ │ └── sheet.tsx │ ├── api │ │ ├── auth │ │ │ ├── [...nextauth] │ │ │ │ └── route.ts │ │ │ ├── bluesky │ │ │ │ └── route.ts │ │ │ ├── mastodon │ │ │ │ └── route.ts │ │ │ ├── refresh │ │ │ │ └── route.ts │ │ │ └── unlink │ │ │ │ └── route.ts │ │ ├── delete │ │ │ └── route.ts │ │ ├── import-status │ │ │ └── [jobId] │ │ │ │ └── route.ts │ │ ├── migrate │ │ │ ├── matching_found │ │ │ │ └── route.ts │ │ │ └── send_follow │ │ │ │ └── route.ts │ │ ├── newsletter │ │ │ ├── follow_bot │ │ │ │ └── route.ts │ │ │ ├── request │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── share │ │ │ ├── bluesky │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── stats │ │ │ ├── route.ts │ │ │ └── total │ │ │ │ └── route.ts │ │ ├── support │ │ │ └── route.ts │ │ ├── tasks │ │ │ └── [taskId] │ │ │ │ └── route.ts │ │ ├── update │ │ │ └── user_stats │ │ │ │ └── route.ts │ │ ├── upload │ │ │ ├── large-files │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ └── users │ │ │ ├── automatic-reconnect │ │ │ └── route.ts │ │ │ ├── bot-newsletter │ │ │ └── route.ts │ │ │ └── language │ │ │ └── route.ts │ ├── auth.config.ts │ ├── auth.ts │ ├── favicon.ico │ ├── fonts │ │ ├── GeistMonoVF.woff │ │ ├── GeistVF.woff │ │ ├── SyneTactile-Regular.ttf │ │ └── plex.ts │ ├── globals.css │ ├── layout.tsx │ └── providers.tsx ├── hooks │ ├── useAuthTokens.ts │ ├── useBotNewsletterState.ts │ ├── useDashboardState.ts │ ├── useMastodonInstances.ts │ ├── useNewsLetter.ts │ ├── useReconnectState.ts │ └── useStats.ts ├── i18n │ ├── navigation.ts │ └── request.ts ├── lib │ ├── bluesky.ts │ ├── dataFormating.ts │ ├── encryption.ts │ ├── log_utils.ts │ ├── repositories │ │ ├── accountRepository.ts │ │ ├── blueskyRepository.ts │ │ ├── matchingRepository.ts │ │ ├── statsRepository.ts │ │ └── userRepository.ts │ ├── services │ │ ├── accountService.ts │ │ ├── blueskyServices.ts │ │ ├── mastodonService.ts │ │ ├── matchingService.ts │ │ ├── newsletterService.ts │ │ ├── statsServices.ts │ │ └── userServices.ts │ ├── states │ │ └── reconnectStates.ts │ ├── supabase-adapter.ts │ ├── supabase.ts │ ├── types │ │ ├── account.ts │ │ ├── bluesky.ts │ │ ├── common.ts │ │ ├── mastodon.ts │ │ ├── matching.ts │ │ ├── stats.ts │ │ └── user.ts │ ├── upload_utils.ts │ └── utils.ts ├── middleware.ts └── services │ └── api.ts ├── supabase ├── .gitignore ├── config.toml ├── functions │ └── process-imports │ │ └── index.ts ├── migrations │ ├── 001_create_schemas.sql │ ├── 002_create_users_table.sql │ ├── 003_create_accounts_table.sql │ ├── 004_create_sessions_table.sql │ ├── 005_create_verification_tokens_table.sql │ ├── 006_create_user_data_table.sql │ ├── 007_create_row_level_security.sql │ ├── 008_update_rls_for_nextauth.sql │ ├── 009_create_table_public_profiles.sql │ ├── 010_create_bluesky_table.sql │ ├── 011_share_events.sql │ ├── 012_create_import_jobs.sql │ ├── 013_create_mastodon_tables.sql │ ├── 014_create_bluesky_mapping.sql │ ├── 015_create_bluesky_twitter.sql │ ├── 017_update_sources.sql │ ├── 018_create_view_migration_bluesky.sql │ ├── 019_create_mastodon_twitter_users.sql │ ├── 020_update_targets_with_mastodon.sql │ ├── 021_create_reconnect_queue.sql │ ├── 022_create_trigger_to_update_sources_targets.sql │ └── 023_create_functions_for_stats.sql ├── migrations_backup │ ├── 001_create_schemas.sql │ ├── 20241217191141_create_schemas.sql │ ├── 20241217191212_create_users_table.sql │ ├── 20241217191213_create_accounts_table.sql │ ├── 20241217191214_create_sessions_table.sql │ ├── 20241217191215_create_verification_tokens_table.sql │ ├── 20241217191216_create_user_data_table.sql │ ├── 20241217191217_create_row_level_security.sql │ ├── 20241217191218_update_rls_for_nextauth.sql │ ├── 20241217191219_create_table_public_profiles.sql │ ├── 20241217191220_create_bluesky_table.sql │ └── 20241217191221_share_events.sql └── seed.sql ├── tailwind.config.js ├── tailwind.config.ts ├── tsconfig.json ├── watcher ├── Dockerfile ├── monitor.log ├── package.json ├── src │ └── monitor.ts └── tsconfig.json ├── worker ├── Dockerfile ├── Dockerfile.dev ├── package.json ├── src │ ├── index.ts │ ├── jobProcessor.ts │ ├── log_utils.ts │ └── utils.ts ├── start-workers.sh └── tsconfig.json └── worker_refreshtoken ├── Dockerfile ├── package-lock.json ├── package.json ├── src └── index.ts └── tsconfig.json /!Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | WORKDIR /app 3 | 4 | # Copier tous les fichiers du projet 5 | COPY . . 6 | 7 | # Installer les dépendances 8 | RUN npm install 9 | 10 | # Set environment variables 11 | ENV NODE_ENV=development 12 | ENV NEXT_TELEMETRY_DISABLED=1 13 | ENV PORT=3000 14 | ENV HOSTNAME="0.0.0.0" 15 | 16 | # Exposer le port 17 | EXPOSE 3000 18 | 19 | # Utiliser npm run dev pour le hot reload 20 | CMD ["npm", "run", "dev"] -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals"], 3 | "rules": { 4 | "react/no-unescaped-entities": "off", 5 | "react-hooks/exhaustive-deps": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.example.env.local: -------------------------------------------------------------------------------- 1 | TWITTER_CLIENT_ID= 2 | TWITTER_CLIENT_SECRET= 3 | 4 | NEXTAUTH_URL= 5 | NEXTAUTH_SECRET=6FOTLhAxDomRh49ordHKPbuyo7RUYt2GZ3moNdTZvOQ 6 | 7 | NEXT_PUBLIC_SUPABASE_URL= 8 | SUPABASE_SERVICE_ROLE_KEY= 9 | NEXT_PUBLIC_SUPABASE_ANON_KEY= 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.* 5 | .yarn/* 6 | !.yarn/patches 7 | !.yarn/plugins 8 | !.yarn/releases 9 | !.yarn/versions 10 | 11 | # testing 12 | /coverage 13 | 14 | # next.js 15 | /.next/ 16 | /out/ 17 | 18 | # production et tests 19 | /build 20 | /tmp 21 | /logs 22 | /volumes 23 | /worker/node_modules 24 | 25 | 26 | # misc 27 | .DS_Store 28 | *.pem 29 | 30 | # debug 31 | npm-debug.log* 32 | yarn-debug.log* 33 | yarn-error.log* 34 | 35 | # env files (can opt-in for committing if needed) 36 | .env* 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescript 42 | *.tsbuildinfo 43 | next-env.d.ts 44 | 45 | # certifs 46 | *.key 47 | *.crt 48 | *.pem 49 | *.csr 50 | backup.sql 51 | 52 | /tests 53 | /volumes 54 | /cleanup 55 | /worker_refreshtoken/node_modules 56 | 57 | 58 | /cerbots 59 | /coverage 60 | /scripts 61 | docker-compose.migration.yml 62 | Dockerfile.migration 63 | Dockerfile 64 | reload.docker-compose.yml 65 | 66 | /nginx 67 | nginx.conf 68 | /nginx/certs 69 | !Dockerfile 70 | /worker/Dockerfile 71 | /watcher/Dockerfile -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM node:20-alpine AS builder 3 | 4 | # Create non-root user 5 | RUN addgroup -g 1001 appgroup && \ 6 | adduser -u 1001 -G appgroup -s /bin/sh -D appuser 7 | 8 | RUN npm install -g npm@11.2.0 9 | 10 | 11 | WORKDIR /app 12 | 13 | # Set ownership of the working directory 14 | RUN chown -R appuser:appgroup /app 15 | 16 | # Copy dependency files with correct ownership 17 | COPY --chown=appuser:appgroup package*.json ./ 18 | 19 | # Create node_modules with correct permissions 20 | RUN mkdir -p node_modules && chown -R appuser:appgroup node_modules 21 | 22 | # Switch to non-root user 23 | USER appuser 24 | 25 | 26 | RUN npm install 27 | 28 | # Copy source with correct ownership 29 | COPY --chown=appuser:appgroup . . 30 | 31 | RUN npm run build 32 | 33 | # Production stage 34 | FROM node:20-alpine AS production 35 | 36 | # Create the same non-root user in production stage 37 | RUN addgroup -g 1001 appgroup && \ 38 | adduser -u 1001 -G appgroup -s /bin/sh -D appuser 39 | 40 | RUN npm install -g npm@11.2.0 41 | 42 | 43 | WORKDIR /app 44 | 45 | 46 | # Set ownership of the working directory 47 | RUN chown -R appuser:appgroup /app 48 | 49 | ENV NODE_ENV production 50 | ENV PORT 3000 51 | ENV HOSTNAME "0.0.0.0" 52 | 53 | # Copy dependency files with correct ownership 54 | COPY --chown=appuser:appgroup package*.json ./ 55 | 56 | # Create node_modules with correct permissions 57 | RUN mkdir -p node_modules && chown -R appuser:appgroup node_modules 58 | 59 | # Switch to non-root user 60 | USER appuser 61 | 62 | 63 | RUN npm install --production 64 | 65 | # Copy built files from builder stage with correct ownership 66 | COPY --chown=appuser:appgroup --from=builder /app/.next ./.next 67 | COPY --chown=appuser:appgroup --from=builder /app/public ./public 68 | COPY --chown=appuser:appgroup --from=builder /app/next.config.js ./ 69 | 70 | EXPOSE 3000 71 | 72 | CMD ["npm", "run", "start"] 73 | 74 | # # Build stage 75 | # FROM node:20-alpine 76 | # WORKDIR /app 77 | 78 | # # Update npm to latest version 79 | # RUN npm install -g npm@11.1.0 80 | 81 | # # Copier uniquement les fichiers nécessaires pour npm install 82 | # COPY package*.json ./ 83 | # COPY tsconfig*.json ./ 84 | # COPY next.config.js ./ 85 | 86 | # # Installer les dépendances avec cache 87 | # RUN npm install 88 | 89 | # # Set environment variables 90 | # ENV NODE_ENV=development 91 | # ENV NEXT_TELEMETRY_DISABLED=1 92 | # ENV PORT=3000 93 | # ENV HOSTNAME="0.0.0.0" 94 | 95 | # # Exposer le port 96 | # EXPOSE 3000 97 | 98 | # # Utiliser npm run dev pour le hot reload 99 | # CMD ["npm", "run", "dev"] -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | WORKDIR /app 3 | 4 | # Copier d'abord les fichiers de dépendances 5 | COPY package*.json ./ 6 | 7 | # Installer les dépendances 8 | RUN npm install 9 | 10 | # Set environment variables 11 | ENV NODE_ENV=development 12 | ENV NEXT_TELEMETRY_DISABLED=1 13 | ENV PORT=3000 14 | ENV HOSTNAME="0.0.0.0" 15 | 16 | # Ne pas copier le code source ici car il sera monté comme volume 17 | # COPY . . 18 | 19 | # Exposer le port 20 | EXPOSE 3000 21 | 22 | # Utiliser npm run dev pour être cohérent avec docker-compose.dev.yml 23 | CMD ["npm", "run", "dev"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 1. Install Supabase & Run Locally 2 | 3 | #### 1. Install Docker 4 | 5 | You will need to install Docker to run Supabase locally. You can download it [here](https://docs.docker.com/get-docker) for free. 6 | 7 | #### 2. Install Supabase CLI 8 | 9 | **MacOS/Linux** 10 | 11 | ```bash 12 | brew install supabase/tap/supabase 13 | ``` 14 | 15 | **Windows** 16 | 17 | ```bash 18 | scoop bucket add supabase https://github.com/supabase/scoop-bucket.git 19 | scoop install supabase 20 | ``` 21 | 22 | #### 3. Start Supabase 23 | 24 | Dans le terminal du repo Git cloné, run les commandes suivantes : 25 | 26 | ```bash 27 | supabase start 28 | ``` 29 | 30 | Appliquer la migration lors du premier lancement et pour redemarer la base de donnees en appliquant les modifications des migrations 31 | 32 | ```bash 33 | supabase db reset 34 | ``` 35 | 36 | #### 4. Environment Variables 37 | 38 | In your terminal at the root of your local Chatbot UI repository, run: 39 | 40 | ```bash 41 | cp .example.env.local .env.local 42 | ``` 43 | 44 | Get the required values by running: 45 | 46 | ```bash 47 | supabase status 48 | ``` 49 | 50 | On doit recuperer les valeurs suivants : 51 | - API URL = NEXT_PUBLIC_SUPABASE_URL 52 | - anon key = NEXT_PUBLIC_SUPABASE_ANON_KEY 53 | - service_role key = SUPABASE_SERVICE_ROLE_KEY 54 | 55 | 56 | ### 2. Getting Started 57 | 58 | ### 1. Creer une application Twitter sur : 59 | ``` https://developer.twitter.com/en/portal/dashboard ``` 60 | 61 | - Selectionner la version de l'API Twitter : OAuth 2.0 62 | - Remplir les champs obligatoires 63 | - Dans la rubrique Keys and Tokens : en bas de la page, il y a la "OAuth 2.0 Client ID and Client Secret", on veut recuperer les valeurs suivantes : 64 | - TWITTER_CLIENT_ID = CLIENT_ID 65 | - TWITTER_CLIENT_SECRET = CLIENT_SECRET 66 | 67 | 68 | Pour les autres variables d'environnement : 69 | 70 | NEXTAUTH_URL= la URL de l'application qui doit etre externe pour permettre la connexion avec l'API Twitter 71 | 72 | ### 2. Lancer le tunnel pour obtenir l'URL de l'application 73 | 74 | Ensuite creer le tunnel pour obtenir l'URL de l'application : 75 | 76 | ```bash 77 | cloudflared tunnel --url http://localhost:3000 78 | ``` 79 | 80 | Recuperer l'URL obtenue et l'ajouter a l'env variable `NEXTAUTH_URL` dans `.env.local` 81 | 82 | ### 3. Se connecter au developper Portal de Twitter et copier l'url de l'application dans les champs suivants : 83 | 84 | User Authentification Settings : 85 | - Callback URI/ Redirect URL : 86 | ``` 87 | [URL TUNNEL]/api/auth/callback/twitter 88 | 89 | ``` 90 | 91 | - Website URL : 92 | ``` 93 | [URL TUNNEL] 94 | ``` 95 | 96 | ### 4. L'application devrait etre accessible ! 97 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | app: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile.dev 8 | env_file: 9 | - .env.local 10 | environment: 11 | - NODE_ENV=development 12 | - WATCHPACK_POLLING=true 13 | - CHOKIDAR_USEPOLLING=true 14 | - NEXT_WEBPACK_USEPOLLING=1 15 | - NEXT_PUBLIC_FORCE_REBUILD=true 16 | ports: 17 | - "3000:3000" 18 | volumes: 19 | - ./:/app:delegated 20 | - /app/node_modules 21 | - /app/.next 22 | - shared-tmp:/app/tmp 23 | networks: 24 | - app_network 25 | extra_hosts: 26 | - "host.docker.internal:host-gateway" 27 | command: npm run dev 28 | stdin_open: true 29 | tty: true 30 | 31 | worker: 32 | build: 33 | context: ./worker 34 | dockerfile: Dockerfile 35 | volumes: 36 | - ./worker:/app 37 | - /app/node_modules 38 | - shared-tmp:/app/tmp 39 | env_file: 40 | - ./worker/.env 41 | environment: 42 | - NODE_ENV=development 43 | networks: 44 | - app_network 45 | extra_hosts: 46 | - "host.docker.internal:host-gateway" 47 | depends_on: 48 | - app 49 | command: npm run dev 50 | 51 | nginx: 52 | image: nginx:alpine 53 | ports: 54 | - "80:80" 55 | - "443:443" 56 | volumes: 57 | - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro 58 | - ./nginx/certs:/etc/nginx/certs:ro 59 | depends_on: 60 | - app 61 | networks: 62 | - app_network 63 | command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'" 64 | 65 | 66 | watcher: 67 | build: 68 | context: ./watcher 69 | dockerfile: Dockerfile 70 | environment: 71 | - NODE_ENV=development 72 | env_file: 73 | - ./watcher/.env 74 | volumes: 75 | - ./watcher:/app 76 | - /app/node_modules 77 | networks: 78 | - app_network 79 | extra_hosts: 80 | - "host.docker.internal:host-gateway" 81 | healthcheck: 82 | test: ["CMD", "wget", "--spider", "-q", "host.docker.internal:54321"] 83 | interval: 30s 84 | timeout: 10s 85 | retries: 3 86 | start_period: 30s 87 | command: npm run dev 88 | 89 | networks: 90 | app_network: 91 | driver: bridge 92 | 93 | volumes: 94 | shared-tmp: -------------------------------------------------------------------------------- /fluent-bit/fluent-bit.conf: -------------------------------------------------------------------------------- 1 | [SERVICE] 2 | Flush 5 3 | Daemon Off 4 | Log_Level info 5 | 6 | [INPUT] 7 | Name forward 8 | Listen 0.0.0.0 9 | Port 24224 10 | 11 | [OUTPUT] 12 | Name es 13 | Match * 14 | Host ${ELASTICSEARCH_HOST} 15 | Port ${ELASTICSEARCH_PORT} 16 | HTTP_User ${ELASTICSEARCH_USER} 17 | HTTP_Passwd ${ELASTICSEARCH_PASSWORD} 18 | Logstash_Format On 19 | Logstash_Prefix logs 20 | Type _doc 21 | Include_Tag_Key On 22 | Tag_Key tag 23 | tls On 24 | tls.verify Off # Changé car Elasticsearch 8.x utilise un certificat auto-signé par défaut 25 | Suppress_Type_Name On 26 | Replace_Dots On 27 | # Ajout des paramètres pour Elasticsearch 8.x 28 | HTTP_Compression On 29 | Net_Protocol http -------------------------------------------------------------------------------- /kong.yml: -------------------------------------------------------------------------------- 1 | _format_version: "2.1" 2 | _transform: true 3 | 4 | services: 5 | - name: meta 6 | url: http://meta:8080 7 | routes: 8 | - name: meta-api 9 | paths: 10 | - /api/ 11 | strip_path: false 12 | - name: meta-root 13 | paths: 14 | - /pg-meta/ 15 | strip_path: true 16 | plugins: 17 | - name: cors 18 | 19 | - name: studio 20 | url: http://studio:3000 21 | routes: 22 | - name: studio-root 23 | paths: 24 | - / 25 | - /project/ 26 | - /css/ 27 | - /monaco-editor/ 28 | - /favicon/ 29 | - /img/ 30 | - /js/ 31 | strip_path: false 32 | - name: studio-api 33 | paths: 34 | - /api/ 35 | strip_path: false 36 | plugins: 37 | - name: cors 38 | 39 | - name: rest 40 | url: http://rest:3000 41 | routes: 42 | - name: rest-all 43 | strip_path: true 44 | paths: 45 | - /rest/v1/ 46 | plugins: 47 | - name: cors 48 | - name: key-auth 49 | config: 50 | key_names: 51 | - apikey 52 | - name: acl 53 | config: 54 | hide_groups_header: true 55 | allow: 56 | - anon 57 | - service_role 58 | 59 | consumers: 60 | - username: anon 61 | keyauth_credentials: 62 | - key: ${ANON_KEY} 63 | acls: 64 | - group: anon 65 | 66 | - username: service_role 67 | keyauth_credentials: 68 | - key: ${SERVICE_ROLE_KEY} 69 | acls: 70 | - group: service_role -------------------------------------------------------------------------------- /next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import "next-auth" 2 | import { DefaultSession, DefaultUser } from "next-auth" 3 | import { JWT } from "next-auth/jwt" 4 | 5 | declare module "next-auth" { 6 | interface Session { 7 | user: { 8 | id: string 9 | has_onboarded: boolean 10 | hqx_newsletter: boolean 11 | oep_accepted: boolean 12 | research_accepted : boolean 13 | have_seen_newsletter: boolean 14 | automatic_reconnect: boolean 15 | have_seen_bot_newsletter: boolean 16 | twitter_id?: string | null 17 | twitter_username?: string | null 18 | twitter_image?: string | null 19 | mastodon_id?: string | null 20 | mastodon_username?: string | null 21 | mastodon_image?: string | null 22 | mastodon_instance?: string | null 23 | bluesky_id?: string | null 24 | bluesky_username?: string | null 25 | bluesky_image?: string | null 26 | } & DefaultSession["user"] 27 | } 28 | 29 | interface User extends DefaultUser { 30 | id: string 31 | has_onboarded: boolean 32 | hqx_newsletter: boolean 33 | oep_accepted: boolean 34 | research_accepted : boolean 35 | have_seen_newsletter: boolean 36 | have_seen_bot_newsletter : boolean 37 | automatic_reconnect: boolean 38 | twitter_id?: string | null 39 | twitter_username?: string | null 40 | twitter_image?: string | null 41 | mastodon_id?: string | null 42 | mastodon_username?: string | null 43 | mastodon_image?: string | null 44 | mastodon_instance?: string | null 45 | bluesky_id?: string | null 46 | bluesky_username?: string | null 47 | bluesky_image?: string | null 48 | } 49 | } 50 | 51 | declare module "next-auth/jwt" { 52 | interface JWT { 53 | id: string 54 | has_onboarded: boolean 55 | hqx_newsletter: boolean 56 | oep_accepted: boolean 57 | research_accepted : boolean 58 | have_seen_newsletter: boolean 59 | have_seen_bot_newsletter : boolean 60 | automatic_reconnect: boolean 61 | twitter_id?: string 62 | twitter_username?: string 63 | twitter_image?: string 64 | mastodon_id?: string 65 | mastodon_username?: string 66 | mastodon_image?: string 67 | mastodon_instance?: string | null 68 | bluesky_id?: string 69 | bluesky_username?: string 70 | bluesky_image?: string 71 | } 72 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withNextIntl = require('next-intl/plugin')('./src/i18n/request.ts'); 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | typescript: { 6 | // ⚠️ Dangereux: Ignore les erreurs TypeScript pendant la production build 7 | ignoreBuildErrors: true, 8 | }, 9 | images: { 10 | domains: ['pbs.twimg.com', 'abs.twimg.com', 'cdn.bsky.app'] 11 | }, 12 | headers: async () => [ 13 | { 14 | source: '/:path*', 15 | headers: [ 16 | { 17 | key: 'Strict-Transport-Security', 18 | value: 'max-age=63072000; includeSubDomains; preload' 19 | }, 20 | { 21 | key: 'X-DNS-Prefetch-Control', 22 | value: 'on' 23 | }, 24 | { 25 | key: 'X-XSS-Protection', 26 | value: '1; mode=block' 27 | }, 28 | { 29 | key: 'X-Frame-Options', 30 | value: 'DENY' 31 | }, 32 | { 33 | key: 'X-Content-Type-Options', 34 | value: 'nosniff' 35 | }, 36 | { 37 | key: 'Referrer-Policy', 38 | value: 'origin-when-cross-origin' 39 | }, 40 | { 41 | key: 'Permissions-Policy', 42 | value: 'camera=(), microphone=(), geolocation=()' 43 | } 44 | ] 45 | } 46 | ], 47 | output: 'standalone', 48 | }; 49 | 50 | module.exports = withNextIntl(nextConfig); -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 1024; 3 | } 4 | 5 | http { 6 | include mime.types; 7 | default_type application/octet-stream; 8 | 9 | # Augmenter la taille maximale des uploads (50MB) 10 | client_max_body_size 900M; 11 | large_client_header_buffers 4 256k; 12 | proxy_buffer_size 512k; 13 | proxy_buffers 16 512k; 14 | proxy_busy_buffers_size 512k; 15 | 16 | upstream nextjs_upstream { 17 | server app:3000; 18 | } 19 | 20 | server { 21 | listen 80; 22 | server_name app.beta.v2.helloquitx.com; 23 | 24 | location /.well-known/acme-challenge/ { 25 | root /var/www/certbot; 26 | } 27 | 28 | location / { 29 | return 301 https://$host$request_uri; 30 | } 31 | } 32 | 33 | server { 34 | listen 443 ssl; 35 | server_name app.beta.v2.helloquitx.com; 36 | 37 | ssl_certificate /etc/nginx/certs/app.beta.v2.helloquitx.com/fullchain.pem; 38 | ssl_certificate_key /etc/nginx/certs/app.beta.v2.helloquitx.com/privkey.pem; 39 | 40 | # Amélioration de la sécurité SSL 41 | ssl_session_timeout 1d; 42 | ssl_session_cache shared:SSL:50m; 43 | ssl_session_tickets off; 44 | 45 | ssl_protocols TLSv1.2 TLSv1.3; 46 | ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; 47 | ssl_prefer_server_ciphers off; 48 | 49 | # HSTS (uncomment if you're sure) 50 | # add_header Strict-Transport-Security "max-age=63072000" always; 51 | 52 | auth_basic "Restricted Area"; 53 | auth_basic_user_file /etc/nginx/auth/.htpasswd; 54 | 55 | location / { 56 | proxy_pass http://nextjs_upstream; 57 | proxy_http_version 1.1; 58 | proxy_set_header Upgrade $http_upgrade; 59 | proxy_set_header Connection 'upgrade'; 60 | proxy_set_header Host $host; 61 | proxy_cache_bypass $http_upgrade; 62 | proxy_set_header X-Real-IP $remote_addr; 63 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 64 | proxy_set_header X-Forwarded-Proto $scheme; 65 | proxy_max_temp_file_size 0; 66 | proxy_headers_hash_max_size 2048; 67 | proxy_headers_hash_bucket_size 512; 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openportability", 3 | "version": "0.1.0", 4 | "private": true, 5 | "engines": { 6 | "node": ">=18.x" 7 | }, 8 | "scripts": { 9 | "dev": "next dev", 10 | "build": "next build", 11 | "start": "next start", 12 | "lint": "next lint" 13 | }, 14 | "dependencies": { 15 | "@atproto/api": "^0.14.9", 16 | "@auth/supabase-adapter": "^1.8.0", 17 | "@emotion/is-prop-valid": "^1.3.1", 18 | "@headlessui/react": "^2.2.0", 19 | "@radix-ui/react-dialog": "^1.1.6", 20 | "@supabase/supabase-js": "^2.49.1", 21 | "@tailwindcss/postcss": "^4.0.0", 22 | "@zip.js/zip.js": "^2.7.57", 23 | "class-variance-authority": "^0.7.1", 24 | "clsx": "^2.1.1", 25 | "crypto-js": "^4.2.0", 26 | "date-fns": "^4.1.0", 27 | "fflate": "^0.8.2", 28 | "framer-motion": "^12.4.7", 29 | "jszip": "^3.10.1", 30 | "lucide-react": "^0.475.0", 31 | "next": "15.3.0", 32 | "next-auth": "^5.0.0-beta.25", 33 | "next-intl": "^3.26.5", 34 | "nodemailer": "^6.10.0", 35 | "react": "^19.0.0", 36 | "react-dom": "^19.0.0", 37 | "react-icons": "^5.5.0", 38 | "sonner": "^2.0.3", 39 | "tailwind-merge": "^3.0.1" 40 | }, 41 | "devDependencies": { 42 | "@types/crypto-js": "^4.2.2", 43 | "@types/node": "^22", 44 | "@types/react": "^19", 45 | "@types/react-dom": "^19", 46 | "eslint": "^9.20.1", 47 | "eslint-config-next": "15.3.0", 48 | "postcss": "^8.5.1", 49 | "tailwindcss": "^4.0.0", 50 | "typescript": "^5" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | module.exports = { 3 | plugins: { 4 | '@tailwindcss/postcss': {}, 5 | }, 6 | }; -------------------------------------------------------------------------------- /public/boats/boat-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 10 | 11 | 12 | 13 | 15 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/boats/boat-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 10 | 11 | 12 | 13 | 15 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /public/boats/boat-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 11 | 12 | 13 | 14 | 16 | 20 | 21 | 22 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /public/boats/boat-4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /public/boats/boat-5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /public/boats/boat-6.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /public/boats/boat-8.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 11 | 12 | 13 | 14 | 16 | 18 | 20 | 22 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /public/boats/boat-9.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /public/client-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "client_id": "https://2f66bda139ff0c.lhr.life/client-metadata.json", 3 | "application_type": "web", 4 | "client_name": "GoodbyeX", 5 | "client_uri": "https://2f66bda139ff0c.lhr.life", 6 | "logo_uri": "https://2f66bda139ff0c.lhr.life/logo.png", 7 | "tos_uri": "https://2f66bda139ff0c.lhr.life/terms", 8 | "policy_uri": "https://2f66bda139ff0c.lhr.life/privacy", 9 | "dpop_bound_access_tokens": true, 10 | "grant_types": [ 11 | "authorization_code", 12 | "refresh_token" 13 | ], 14 | "redirect_uris": [ 15 | "https://2f66bda139ff0c.lhr.life/api/auth/callback/bluesky" 16 | ], 17 | "response_types": [ 18 | "code" 19 | ], 20 | "scope": "atproto", 21 | "token_endpoint_auth_method": "none", 22 | "authorization_endpoint": "https://bsky.social/oauth/authorize", 23 | "token_endpoint": "https://bsky.social/oauth/token", 24 | "userinfo_endpoint": "https://bsky.social/xrpc/com.atproto.server.getSession", 25 | "pushed_authorization_request_endpoint": "https://bsky.social/oauth/par", 26 | "require_pushed_authorization_requests": true, 27 | "client_secret_expires_at": 0 28 | } -------------------------------------------------------------------------------- /public/compass/needle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ISCPIF/OpenPortability/e2ab74e39c25954a6387d06252854863e89ff19b/public/logo.png -------------------------------------------------------------------------------- /public/newSVG/BS.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/newSVG/X.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/newSVG/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/newSVG/masto.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/newSVG/notif.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/newSVG/steps-Init.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/newSVG/steps-OK.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/newSVG/steps-partiel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/newSVG/uil_arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/newSVG/uil_arrow-growth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/newSVG/upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/newSVG/video.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/sea-wave.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /public/sea.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /public/v2/Pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/v2/badge-success-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ISCPIF/OpenPortability/e2ab74e39c25954a6387d06252854863e89ff19b/public/v2/badge-success-1.png -------------------------------------------------------------------------------- /public/v2/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/v2/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/v2/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/v2/statut=BS-defaut.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/v2/statut=BS-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/v2/statut=Masto-Defaut.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/v2/statut=Masto-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/v2/uil_arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/v2/uil_arrow-growth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python_worker/Dockerfile: -------------------------------------------------------------------------------- 1 | # python_worker/Dockerfile 2 | FROM node:20-alpine 3 | 4 | RUN addgroup -g 1001 appgroup && \ 5 | adduser -u 1001 -G appgroup -s /bin/sh -D appuser 6 | 7 | # Install Python and pip 8 | RUN apk add --no-cache python3 py3-pip bash 9 | 10 | # Create Python virtual environment 11 | ENV VIRTUAL_ENV=/opt/venv 12 | RUN python3 -m venv $VIRTUAL_ENV 13 | ENV PATH="$VIRTUAL_ENV/bin:$PATH" 14 | 15 | WORKDIR /app 16 | 17 | # First, copy only package files to leverage Docker cache 18 | COPY --chown=appuser:appgroup package*.json ./ 19 | COPY --chown=appuser:appgroup tsconfig.json ./ 20 | 21 | # Install Node.js dependencies 22 | RUN npm install 23 | 24 | # Then copy the rest of the application 25 | COPY --chown=appuser:appgroup . . 26 | 27 | # Install Python dependencies 28 | RUN pip install --no-cache-dir -r requirements.txt 29 | 30 | RUN chown -R appuser:appgroup /app 31 | 32 | USER appuser 33 | 34 | # Build TypeScript 35 | RUN npm run build 36 | 37 | # Make Python scripts executable 38 | RUN chmod +x *.py 39 | RUN chmod +x start-py-workers.sh 40 | 41 | # Environment variables 42 | ENV PYTHON_WORKER_ID="python-worker-01" 43 | ENV PYTHON_WORKER_POLLING_INTERVAL=5000 44 | ENV PYTHON_WORKER_STALLED_TASK_TIMEOUT=300000 45 | ENV NODE_ENV=production 46 | 47 | CMD ["/bin/bash", "./start-py-workers.sh"] -------------------------------------------------------------------------------- /python_worker/messages/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "testDm": "Dies ist eine automatische Testnachricht von OpenPortability, um zu überprüfen, ob wir Sie per DM erreichen können. Es ist keine Aktion erforderlich.", 3 | "recoNewsletter": { 4 | "singular": "Hallo! Es gibt ${count} Person, der Sie auf Twitter gefolgt sind und die jetzt auf ${platformName} ist! Besuchen Sie openportability.org, um sie zu finden 🚀", 5 | "plural": "Hallo! Es gibt ${count} Personen, denen Sie auf Twitter gefolgt sind und die jetzt auf ${platformName} sind! Besuchen Sie openportability.org, um sie zu finden 🚀" 6 | } 7 | } -------------------------------------------------------------------------------- /python_worker/messages/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "testDm": "This is an automated test message from OpenPortability to verify we can reach you via DM. No action is required.", 3 | "recoNewsletter": { 4 | "singular": "Hello! There is ${count} person you followed on Twitter who is now on ${platformName}! Visit openportability.org to find them 🚀", 5 | "plural": "Hello! There are ${count} people you followed on Twitter who are now on ${platformName}! Visit openportability.org to find them 🚀" 6 | } 7 | } -------------------------------------------------------------------------------- /python_worker/messages/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "testDm": "Este es un mensaje de prueba automático de OpenPortability para verificar que podemos contactarte por MD. No se requiere ninguna acción.", 3 | "recoNewsletter": { 4 | "singular": "¡Hola! Hay ${count} persona que seguías en Twitter y que ahora está en ${platformName}. ¡Visita openportability.org para encontrarla 🚀", 5 | "plural": "¡Hola! Hay ${count} personas que seguías en Twitter y que ahora están en ${platformName}. ¡Visita openportability.org para encontrarlas 🚀" 6 | } 7 | } -------------------------------------------------------------------------------- /python_worker/messages/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "testDm": "Ceci est un message de test automatique d'OpenPortability pour vérifier que nous pouvons vous contacter via DM. Aucune action n'est requise.", 3 | "recoNewsletter": { 4 | "singular": "Bonjour ! Il y a ${count} personne que vous suiviez sur Twitter et qui est maintenant sur ${platformName} ! Rendez-vous sur openportability.org pour la retrouver 🚀", 5 | "plural": "Bonjour ! Il y a ${count} personnes que vous suiviez sur Twitter et qui sont maintenant sur ${platformName} ! Rendez-vous sur openportability.org pour les retrouver 🚀" 6 | } 7 | } -------------------------------------------------------------------------------- /python_worker/messages/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "testDm": "Questo è un messaggio di prova automatico da OpenPortability per verificare che possiamo raggiungerti tramite DM. Non è richiesta alcuna azione.", 3 | "recoNewsletter": { 4 | "singular": "Ciao! C'è ${count} persona che seguivi su Twitter e che ora è su ${platformName}! Visita openportability.org per trovarla 🚀", 5 | "plural": "Ciao! Ci sono ${count} persone che seguivi su Twitter e che ora sono su ${platformName}! Visita openportability.org per trovarle 🚀" 6 | } 7 | } -------------------------------------------------------------------------------- /python_worker/messages/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "testDm": "Esta é uma mensagem de teste automática da OpenPortability para verificar se podemos contatá-lo(a) via DM. Nenhuma ação é necessária.", 3 | "recoNewsletter": { 4 | "singular": "Olá! Há ${count} pessoa que você seguia no Twitter e que agora está no ${platformName}! Visite openportability.org para encontrá-la 🚀", 5 | "plural": "Olá! Há ${count} pessoas que você seguia no Twitter e que agora estão no ${platformName}! Visite openportability.org para encontrá-las 🚀" 6 | } 7 | } -------------------------------------------------------------------------------- /python_worker/messages/sv.json: -------------------------------------------------------------------------------- 1 | { 2 | "testDm": "Detta är ett automatiskt testmeddelande från OpenPortability för att verifiera att vi kan nå dig via DM. Ingen åtgärd krävs.", 3 | "recoNewsletter": { 4 | "singular": "Hej! Det finns ${count} person som du följde på Twitter och som nu finns på ${platformName}! Besök openportability.org för att hitta hen 🚀", 5 | "plural": "Hej! Det finns ${count} personer som du följde på Twitter och som nu finns på ${platformName}! Besök openportability.org för att hitta dem 🚀" 6 | } 7 | } -------------------------------------------------------------------------------- /python_worker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "python-worker", 3 | "version": "1.0.0", 4 | "main": "dist/index.js", 5 | "scripts": { 6 | "build": "tsc", 7 | "start": "ts-node src/index.ts", 8 | "start:workers": "./start-py-workers.sh", 9 | "dev": "ts-node src/index.ts" 10 | }, 11 | "dependencies": { 12 | "@supabase/supabase-js": "^2.0.0", 13 | "dotenv": "^16.0.0", 14 | "date-fns": "^2.30.0" 15 | 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^18.19.68", 19 | "@types/dotenv": "^8.2.0", 20 | "ts-node": "^10.9.2", 21 | "typescript": "^4.9.5" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /python_worker/requirements.txt: -------------------------------------------------------------------------------- 1 | atproto==0.0.59 2 | requests==2.32.3 3 | httpx==0.26.0 4 | python-dotenv==1.0.1 5 | supabase==2.13.0 6 | Mastodon.py==1.8.1 7 | -------------------------------------------------------------------------------- /python_worker/start-py-workers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Nombre de workers Python à lancer 4 | NUM_WORKERS=2 # Ajustez ce nombre selon vos besoins 5 | 6 | # Array pour stocker les PIDs 7 | declare -a PY_WORKER_PIDS=() 8 | 9 | echo "Starting ${NUM_WORKERS} Python workers..." 10 | 11 | for i in $(seq 1 $NUM_WORKERS); do 12 | # Définir un ID unique pour chaque worker Python 13 | export PYTHON_WORKER_ID="pyworker_${i}" 14 | 15 | # Lancer le worker avec npx ts-node au lieu de node 16 | npx ts-node src/index.ts & 17 | 18 | # Stocker le PID 19 | PY_WORKER_PIDS+=($!) 20 | echo "Started Python worker ${i} with ID ${PYTHON_WORKER_ID} and PID ${PY_WORKER_PIDS[-1]}" 21 | 22 | # Petite pause pour éviter de surcharger au démarrage (optionnel) 23 | sleep 0.5 24 | done 25 | 26 | # Fonction pour arrêter proprement les workers 27 | cleanup() { 28 | echo "Received termination signal. Stopping Python workers..." 29 | for pid in "${PY_WORKER_PIDS[@]}"; do 30 | echo "Sending SIGTERM to PID $pid..." 31 | kill -SIGTERM $pid # Envoyer SIGTERM pour un arrêt gracieux 32 | done 33 | # Attendre un peu que les processus se terminent 34 | sleep 5 35 | # Forcer l'arrêt si nécessaire (optionnel) 36 | # for pid in "${PY_WORKER_PIDS[@]}"; do 37 | # if kill -0 $pid 2>/dev/null; then 38 | # echo "Forcing shutdown for PID $pid..." 39 | # kill -SIGKILL $pid 40 | # fi 41 | # done 42 | echo "Cleanup finished." 43 | exit 0 44 | } 45 | 46 | # Intercepter les signaux d'arrêt (Ctrl+C, etc.) 47 | trap cleanup SIGINT SIGTERM 48 | 49 | echo "Waiting for Python workers to complete (Press Ctrl+C to stop)..." 50 | # Attendre que tous les workers se terminent (ou que le script soit interrompu) 51 | for pid in "${PY_WORKER_PIDS[@]}"; do 52 | wait $pid 53 | done 54 | 55 | echo "All Python workers completed naturally." 56 | -------------------------------------------------------------------------------- /python_worker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "lib": ["es2020"], 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "outDir": "dist", 11 | "rootDir": "src", 12 | "types": ["node"] 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules"] 16 | } -------------------------------------------------------------------------------- /src/app/[locale]/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from "@/app/auth" 2 | import { redirect } from "next/navigation" 3 | 4 | export default async function DashboardLayout({ 5 | children, 6 | }: { 7 | children: React.ReactNode 8 | }) { 9 | const session = await auth() 10 | 11 | if (!session?.user) { 12 | redirect("/auth/signin") 13 | } 14 | 15 | return <>{children} 16 | } 17 | -------------------------------------------------------------------------------- /src/app/[locale]/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import { Inter, Space_Grotesk } from 'next/font/google' 4 | import "../globals.css"; 5 | import { Providers } from "../providers"; 6 | import { auth } from "@/app/auth"; 7 | import { MotionWrapper } from "../_components/MotionWrapper"; 8 | import { NextIntlClientProvider } from 'next-intl'; 9 | import { getMessages } from 'next-intl/server'; 10 | import { Toaster } from 'sonner'; 11 | 12 | const geistSans = localFont({ 13 | src: "../fonts/GeistVF.woff", 14 | variable: "--font-geist-sans", 15 | weight: "100 900", 16 | }); 17 | 18 | const geistMono = localFont({ 19 | src: "../fonts/GeistMonoVF.woff", 20 | variable: "--font-geist-mono", 21 | weight: "100 900", 22 | }); 23 | 24 | const inter = Inter({ subsets: ['latin'], variable: '--font-inter' }) 25 | const spaceGrotesk = Space_Grotesk({ 26 | subsets: ['latin'], 27 | variable: '--font-space-grotesk' 28 | }) 29 | 30 | export const metadata: Metadata = { 31 | title: "OpenPortability", 32 | description: "Libérez vos espaces numériques", 33 | }; 34 | 35 | async function getSession() { 36 | return await auth(); 37 | } 38 | 39 | type Props = { 40 | children: React.ReactNode; 41 | params: Promise<{ locale: string }>; 42 | }; 43 | 44 | export default async function RootLayout({ children, params }: Props) { 45 | const messages = await getMessages(); 46 | const session = await getSession(); 47 | const { locale } = await params; 48 | 49 | return ( 50 | 51 | 52 | 53 | 54 |
55 | 56 | {children} 57 | 58 |
59 | 60 |
61 |
62 | 63 | 64 | ); 65 | } -------------------------------------------------------------------------------- /src/app/[locale]/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import { auth } from "../auth"; 3 | import { getLocale } from 'next-intl/server'; 4 | 5 | // Indiquer à Next.js que cette page est dynamique 6 | export const dynamic = 'force-dynamic'; 7 | export const fetchCache = 'force-no-store'; 8 | 9 | export default async function Home() { 10 | // console.log("🏠 [Home] Starting home page render..."); 11 | 12 | try { 13 | const session = await auth(); 14 | // console.log("🔑 [Home] Session:", session ? "Found" : "Not found"); 15 | 16 | const locale = await getLocale(); 17 | // console.log("🌍 [Home] Current locale:", locale); 18 | 19 | if (!session) { 20 | // console.log("➡️ [Home] Redirecting to signin page..."); 21 | redirect(`/${locale}/auth/signin`); 22 | } 23 | 24 | if (!session.user.has_onboarded){ 25 | // console.log("➡️ [Home] Redirecting to dashboard..."); 26 | redirect(`/${locale}/dashboard`); 27 | } 28 | else 29 | { 30 | redirect(`/${locale}/reconnect`); 31 | } 32 | } catch (error) { 33 | console.error("❌ [Home] Error:", error); 34 | throw error; 35 | } 36 | } -------------------------------------------------------------------------------- /src/app/[locale]/privacy_policy/layout.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from "@/app/auth" 2 | import { redirect } from "next/navigation" 3 | 4 | export default async function PrivaryPolicyLayout({ 5 | children, 6 | }: { 7 | children: React.ReactNode 8 | }) { 9 | 10 | return <>{children} 11 | } 12 | -------------------------------------------------------------------------------- /src/app/[locale]/reconnect/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { plex } from '../../fonts/plex'; 3 | 4 | export default function MigrateLayout({ 5 | children, 6 | }: { 7 | children: ReactNode; 8 | }) { 9 | return ( 10 |
11 | {children} 12 |
13 | ); 14 | } -------------------------------------------------------------------------------- /src/app/[locale]/settings/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { plex } from '../../fonts/plex'; 3 | 4 | export default function MigrateLayout({ 5 | children, 6 | }: { 7 | children: ReactNode; 8 | }) { 9 | return ( 10 |
11 | {children} 12 |
13 | ); 14 | } -------------------------------------------------------------------------------- /src/app/_components/AppLayout.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import React from 'react'; 3 | import Header from "./Header"; 4 | import Footer from "./Footer"; 5 | import LoadingIndicator from "./LoadingIndicator"; 6 | 7 | interface AppLayoutProps { 8 | children: React.ReactNode; 9 | isLoading?: boolean; 10 | loadingMessage?: string; 11 | } 12 | 13 | export default function AppLayout({ children, isLoading, loadingMessage = 'Loading...' }: AppLayoutProps) { 14 | if (isLoading) { 15 | return ( 16 |
17 |
18 |
19 |
20 | 21 |
22 |
23 |
24 |
25 | ); 26 | } 27 | 28 | return ( 29 |
30 |
31 |
32 |
33 | {children} 34 |
36 | ); 37 | } -------------------------------------------------------------------------------- /src/app/_components/BlueSkyLoginButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { motion } from "framer-motion" 4 | import { SiBluesky } from 'react-icons/si' 5 | import { plex } from "@/app/fonts/plex" 6 | import { useTranslations } from 'next-intl' 7 | 8 | interface BlueSkyLoginButtonProps { 9 | onLoadingChange?: (isLoading: boolean) => void; 10 | isConnected?: boolean; 11 | isSelected?: boolean; 12 | className?: string; 13 | onClick?: () => void; 14 | } 15 | 16 | const itemVariants = { 17 | hidden: { opacity: 0, y: -8, scale: 0.95 }, 18 | visible: { 19 | opacity: 1, 20 | y: 0, 21 | scale: 1, 22 | transition: { 23 | type: "spring", 24 | stiffness: 400, 25 | damping: 30 26 | } 27 | } 28 | } 29 | 30 | export default function BlueSkyLoginButton({ 31 | onLoadingChange = () => { }, 32 | isConnected = false, 33 | isSelected = false, 34 | className = "", 35 | onClick = () => {} 36 | }: BlueSkyLoginButtonProps) { 37 | const t = useTranslations('dashboardLoginButtons') 38 | 39 | return ( 40 | 53 | 54 | 55 | {isConnected ? t('connected') : t('services.bluesky')} 56 | 57 | 58 | ) 59 | } -------------------------------------------------------------------------------- /src/app/_components/ConnectedServicesInfo.tsx: -------------------------------------------------------------------------------- 1 | // interface ConnectedServicesInfoProps { 2 | // session: any; 3 | // } 4 | 5 | // export default function ConnectedServicesInfo({ session }: ConnectedServicesInfoProps) { 6 | // const hasMastodon = session?.user?.mastodon_id; 7 | // const hasBluesky = session?.user?.bluesky_id; 8 | // const hasTwitter = session?.user?.twitter_id; 9 | // const hasOnboarded = session?.user?.has_onboarded; 10 | 11 | // const connectedServicesCount = [hasMastodon, hasBluesky, hasTwitter].filter(Boolean).length; 12 | 13 | // return { hasMastodon, hasBluesky, hasTwitter, hasOnboarded, connectedServicesCount }; 14 | // } -------------------------------------------------------------------------------- /src/app/_components/ConsentModal.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useTranslations } from 'next-intl'; 4 | 5 | interface ConsentModalProps { 6 | isOpen: boolean; 7 | onAccept: () => void; 8 | onDecline: () => void; 9 | } 10 | 11 | export default function ConsentModal({ isOpen, onAccept, onDecline }: ConsentModalProps) { 12 | const t = useTranslations('consentModal'); 13 | 14 | if (!isOpen) return null; 15 | 16 | return ( 17 |
18 |
19 |
20 | 27 | 28 | 29 |

30 | {t('title')} 31 |

32 |
33 | 34 |
35 |

36 | {t('description')} 37 |

38 |
    39 |
  • {t('conditions.ownership')}
  • 40 |
  • {t('conditions.cnrsUsage')}
  • 41 |
  • {t('conditions.profileCreation')}
  • 42 |
43 |
44 | 45 |
46 | 52 | 58 |
59 |
60 |
61 | ); 62 | } -------------------------------------------------------------------------------- /src/app/_components/LoadingIndicator.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from "react" 4 | import Image from 'next/image'; 5 | import { motion, AnimatePresence } from "framer-motion" 6 | 7 | import { plex } from '@/app/fonts/plex'; 8 | 9 | import compass from '../../../public/compass/loader-compass.svg'; 10 | import needle from '../../../public/compass/needle.svg'; 11 | 12 | function randomRange(min: number, max: number) { 13 | return min + Math.random() * (max - min); 14 | } 15 | 16 | interface LoadingIndicatorProps { 17 | msg: string; 18 | textSize?: 'sm' | 'base'; 19 | } 20 | 21 | export default function LoadingIndicator({ msg, textSize = 'sm' }: LoadingIndicatorProps) { 22 | const [nextAngle, setNextAngle] = useState(0); 23 | return ( 24 |
25 |
26 | 33 | { 52 | setNextAngle(Math.random() * 360); 53 | }} 54 | > 55 | 63 | 64 |
65 |

66 | {msg} 67 |

68 |
69 | ); 70 | } -------------------------------------------------------------------------------- /src/app/_components/MatchedBlueSkyProfiles.tsx: -------------------------------------------------------------------------------- 1 | // 'use client' 2 | 3 | // import { motion } from 'framer-motion' 4 | // import { SiBluesky } from "react-icons/si" 5 | 6 | // type MatchedProfile = { 7 | // bluesky_handle: string 8 | // } 9 | 10 | // export default function MatchedBlueSkyProfiles({ 11 | // profiles 12 | // }: { 13 | // profiles: MatchedProfile[] 14 | // }) { 15 | // if (!profiles.length) return ( 16 | //
17 | // Aucune correspondance BlueSky trouvée 18 | //
19 | // ) 20 | 21 | // return ( 22 | //
23 | 24 | //
25 | //
26 | 27 | //
28 | // {profiles.map((profile, index) => ( 29 | // 36 | //
37 | //
38 | // 39 | //
40 | 41 | //
42 | //

43 | // {profile.bluesky_handle} 44 | //

45 | //
46 | //
47 | //
48 | // ))} 49 | //
50 | //
51 | // ) 52 | // } -------------------------------------------------------------------------------- /src/app/_components/MotionWrapper.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { MotionConfig } from 'framer-motion'; 4 | import { ReactNode } from 'react'; 5 | 6 | interface MotionWrapperProps { 7 | children: ReactNode; 8 | } 9 | 10 | export function MotionWrapper({ children }: MotionWrapperProps) { 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | } -------------------------------------------------------------------------------- /src/app/_components/RefreshTokenModale.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from 'react' 4 | import { useTranslations } from 'next-intl' 5 | import { motion, AnimatePresence } from 'framer-motion' 6 | import { plex } from '@/app/fonts/plex' 7 | import DashboardLoginButtons from './DashboardLoginButtons' 8 | 9 | type Provider = 'bluesky' | 'mastodon' 10 | 11 | interface RefreshTokenModaleProps { 12 | providers: Provider[] 13 | mastodonInstances: string[] 14 | onClose?: () => void 15 | } 16 | 17 | const containerVariants = { 18 | hidden: { opacity: 0, scale: 0.95 }, 19 | visible: { 20 | opacity: 1, 21 | scale: 1, 22 | transition: { 23 | staggerChildren: 0.1, 24 | delayChildren: 0.2 25 | } 26 | } 27 | } 28 | 29 | export default function RefreshTokenModale({ providers, mastodonInstances, onClose }: RefreshTokenModaleProps) { 30 | const [isLoading, setIsLoading] = useState(false) 31 | const t = useTranslations('refreshToken') 32 | 33 | // Convert providers array to connectedServices object 34 | // Si un provider est dans le tableau providers, il n'est PAS connecté 35 | const connectedServices = { 36 | bluesky: true, 37 | mastodon: true, 38 | ...Object.fromEntries(providers.map(provider => [provider, false])) 39 | } 40 | 41 | return ( 42 |
43 | 49 |

{t('title')}

50 |

{t('description')}

51 | 52 | 58 | 59 | 65 |
66 |
67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /src/app/_components/TwitterRateLimit.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { motion } from "framer-motion" 4 | import { SiMastodon, SiBluesky } from 'react-icons/si' 5 | import { plex } from "@/app/fonts/plex" 6 | 7 | interface TwitterRateLimitProps { 8 | onShowAlternatives: () => void; 9 | } 10 | 11 | export default function TwitterRateLimit({ onShowAlternatives }: TwitterRateLimitProps) { 12 | return ( 13 | 18 |
19 |
20 | 21 | 22 | 23 |
24 |
25 |

26 | Twitter est temporairement indisponible 27 |

28 |
29 |

En raison d'un grand nombre de requêtes, Twitter n'est pas accessible pour le moment.

30 |

Vous pouvez :

31 |
    32 |
  • Réessayer dans quelques minutes
  • 33 |
  • Utiliser une autre plateforme pour vous connecter
  • 34 |
35 |
36 |
37 | 47 |
48 |
49 |
50 |
51 | ) 52 | } -------------------------------------------------------------------------------- /src/app/_components/dashboard/TutorialSection.tsx: -------------------------------------------------------------------------------- 1 | // src/app/_components/dashboard/TutorialSection.tsx 2 | import { motion } from 'framer-motion'; 3 | import { Play } from 'lucide-react'; 4 | import { plex } from '@/app/fonts/plex'; 5 | import { useTranslations } from 'next-intl'; 6 | import { useParams } from 'next/navigation'; 7 | 8 | export default function TutorialSection() { 9 | const params = useParams(); 10 | const t = useTranslations('dashboard'); 11 | 12 | return ( 13 |
14 |

15 | {t('tutorial.title')} 16 |

17 | 27 | 28 | 29 | {t('tutorial.watchVideo')} 30 | 31 | 32 |
33 | ); 34 | } -------------------------------------------------------------------------------- /src/app/_components/reconnect/ReconnectOptionsSections.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | import { useTranslations } from 'next-intl'; 3 | import { GlobalStats } from '@/lib/types/stats'; 4 | 5 | type ReconnectOptionsSectionProps = { 6 | onAutomatic: () => void; 7 | onManual: () => void; 8 | globalStats?: GlobalStats; 9 | has_onboarded?: boolean; 10 | }; 11 | 12 | export default function ReconnectOptionsSection({ 13 | onAutomatic, 14 | onManual, 15 | globalStats, 16 | has_onboarded 17 | }: ReconnectOptionsSectionProps) { 18 | const t = useTranslations('migrate'); 19 | 20 | return ( 21 |
22 |

23 | {t('choose_reconnection_mode')} 24 |

25 | 26 |
27 | 33 |

{t('automatic_mode')}

34 |

{t('automatic_description')}

35 |
36 | 37 | 43 |

{t('manual_mode')}

44 |

{t('manual_description')}

45 |
46 |
47 | 48 | {globalStats && ( 49 |
50 |

51 | {t('total_users_reconnected', { 52 | count: globalStats.users?.total || 0 53 | })} 54 |

55 |
56 | )} 57 |
58 | ); 59 | } -------------------------------------------------------------------------------- /src/app/_components/reconnect/states/AutomaticReconnectionState.tsx: -------------------------------------------------------------------------------- 1 | // src/app/_components/reconnect/states/AutomaticReconnectionState.tsx 2 | import { motion } from 'framer-motion'; 3 | import AutomaticReconnexion from '@/app/_components/AutomaticReconnexion'; 4 | 5 | type AutomaticReconnectionStateProps = { 6 | session: any; 7 | stats: any; 8 | migrationResults: any; 9 | handleAutomaticReconnection: () => void; 10 | }; 11 | 12 | export default function AutomaticReconnectionState({ 13 | session, 14 | stats, 15 | migrationResults, 16 | handleAutomaticReconnection, 17 | }: AutomaticReconnectionStateProps) { 18 | return ( 19 | 25 | 40 | 41 | ); 42 | } -------------------------------------------------------------------------------- /src/app/_components/reconnect/states/LoadingState.tsx: -------------------------------------------------------------------------------- 1 | // src/app/_components/reconnect/states/LoadingState.tsx 2 | import LoadingIndicator from '@/app/_components/LoadingIndicator'; 3 | import { useTranslations } from 'next-intl'; 4 | 5 | export default function LoadingState() { 6 | const t = useTranslations('migrate'); 7 | 8 | return ( 9 |
10 |
11 |
12 |
13 | 14 |
15 |
16 |
17 |
18 | ); 19 | } -------------------------------------------------------------------------------- /src/app/_components/reconnect/states/ManualReconnectionState.tsx: -------------------------------------------------------------------------------- 1 | // src/app/_components/reconnect/states/ManualReconnectionState.tsx 2 | import { motion } from 'framer-motion'; 3 | import ManualReconnexion from '@/app/_components/ManualReconnexion'; 4 | 5 | type ManualReconnectionStateProps = { 6 | session: any; 7 | accountsToProcess: any[]; 8 | handleStartMigration: (accounts: string[]) => void; 9 | handleAutomaticReconnection: () => void; 10 | }; 11 | 12 | export default function ManualReconnectionState({ 13 | session, 14 | accountsToProcess, 15 | handleStartMigration, 16 | handleAutomaticReconnection, 17 | }: ManualReconnectionStateProps) { 18 | return ( 19 | 25 | 36 | 37 | ); 38 | } -------------------------------------------------------------------------------- /src/app/_components/reconnect/states/MissingTokenState.tsx: -------------------------------------------------------------------------------- 1 | // src/app/_components/reconnect/states/MissingTokenState.tsx 2 | import { useTranslations } from 'next-intl'; 3 | import DashboardLoginButtons from '@/app/_components/DashboardLoginButtons'; 4 | 5 | type MissingTokenStateProps = { 6 | session: any; 7 | stats: any; 8 | mastodonInstances: string[]; 9 | setIsLoading: (value: boolean) => void; 10 | missingProviders: string[]; 11 | }; 12 | 13 | export default function MissingTokenState({ 14 | session, 15 | stats, 16 | mastodonInstances, 17 | setIsLoading, 18 | missingProviders, 19 | }: MissingTokenStateProps) { 20 | const t = useTranslations('refreshToken'); 21 | 22 | return ( 23 |
24 |

25 | {t('title')} 26 |

27 | 28 |
29 | 42 |
43 |
44 | ); 45 | } -------------------------------------------------------------------------------- /src/app/_components/reconnect/states/NoConnectedServicesState.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTranslations } from 'next-intl'; 3 | import DashboardLoginButtons from '@/app/_components/DashboardLoginButtons'; 4 | 5 | type NoConnectedServicesStateProps = { 6 | session: any; 7 | stats: any; 8 | mastodonInstances: string[]; 9 | setIsLoading: (value: boolean) => void; 10 | }; 11 | 12 | export default function NoConnectedServicesState({ 13 | session, 14 | stats, 15 | mastodonInstances, 16 | setIsLoading, 17 | }: NoConnectedServicesStateProps) { 18 | const t = useTranslations('migrate'); 19 | 20 | // Calculer le nombre total de comptes non suivis 21 | const totalNotFollowedCount = 22 | (stats?.matches?.bluesky?.notFollowed || 0) + 23 | (stats?.matches?.mastodon?.notFollowed || 0); 24 | 25 | // Détermine quelle clé de traduction utiliser en fonction de has_onboarded 26 | const messageKey = session?.user?.has_onboarded ? 'needBothAccounts' : 'noUploadYet'; 27 | 28 | return ( 29 |
30 |

31 | {t.raw(messageKey).split('{count}').map((part, index, parts) => ( 32 | 33 | {part} 34 | {index < parts.length - 1 && ( 35 | {totalNotFollowedCount} 36 | )} 37 | 38 | ))} 39 |

40 | 41 |
42 | 55 |
56 |
57 | ); 58 | } -------------------------------------------------------------------------------- /src/app/_components/reconnect/states/PartialConnectedServicesState.tsx: -------------------------------------------------------------------------------- 1 | // src/app/_components/reconnect/states/PartialConnectedServicesState.tsx 2 | import { useTranslations } from 'next-intl'; 3 | import DashboardLoginButtons from '@/app/_components/DashboardLoginButtons'; 4 | 5 | type PartialConnectedServicesStateProps = { 6 | session: any; 7 | stats: any; 8 | mastodonInstances: string[]; 9 | setIsLoading: (value: boolean) => void; 10 | }; 11 | 12 | export default function PartialConnectedServicesState({ 13 | session, 14 | stats, 15 | mastodonInstances, 16 | setIsLoading, 17 | }: PartialConnectedServicesStateProps) { 18 | const t = useTranslations('migrate'); 19 | 20 | return ( 21 |
22 |
23 |

24 | {t('connect_missing_service')} 25 |

26 | 27 |
28 | 41 |
42 |
43 |
44 | ); 45 | } -------------------------------------------------------------------------------- /src/app/_components/reconnect/states/ReconnectionCompleteState.tsx: -------------------------------------------------------------------------------- 1 | // src/app/_components/reconnect/states/ReconnectionCompleteState.tsx 2 | import { motion } from 'framer-motion'; 3 | import SuccessAutomaticReconnexion from '@/app/_components/SuccessAutomaticReconnexion'; 4 | 5 | type ReconnectionCompleteStateProps = { 6 | session: any; 7 | stats: any; 8 | globalStats: any; 9 | handleAutomaticReconnection: () => void; 10 | handleManualReconnection: () => void; 11 | refreshStats: () => void; 12 | }; 13 | 14 | export default function ReconnectionCompleteState({ 15 | session, 16 | stats, 17 | globalStats, 18 | handleAutomaticReconnection, 19 | handleManualReconnection, 20 | refreshStats, 21 | }: ReconnectionCompleteStateProps) { 22 | return ( 23 | 29 | 42 | 43 | ); 44 | } -------------------------------------------------------------------------------- /src/app/_components/reconnect/states/ShowOptionsState.tsx: -------------------------------------------------------------------------------- 1 | // src/app/_components/reconnect/states/ShowOptionsState.tsx 2 | import { motion } from 'framer-motion'; 3 | import ReconnexionOptions from '@/app/_components/ReconnexionOptions'; 4 | 5 | type ShowOptionsStateProps = { 6 | session: any; 7 | globalStats: any; 8 | handleAutomaticReconnection: () => void; 9 | handleManualReconnection: () => void; 10 | }; 11 | 12 | export default function ShowOptionsState({ 13 | session, 14 | globalStats, 15 | handleAutomaticReconnection, 16 | handleManualReconnection, 17 | }: ShowOptionsStateProps) { 18 | return ( 19 | 25 | 31 | 32 | ); 33 | } -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { authConfig } from "@/app/auth.config" 2 | import NextAuth from "next-auth" 3 | import { handlers } from "@/app/auth" 4 | 5 | export const { GET, POST } = handlers -------------------------------------------------------------------------------- /src/app/api/auth/mastodon/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { supabase } from '@/lib/supabase' 3 | import logger, { withLogging } from '@/lib/log_utils' 4 | 5 | async function mastodonHandler() { 6 | try { 7 | const { data, error } = await supabase 8 | .from('mastodon_instances') 9 | .select('instance') 10 | .order('instance') 11 | 12 | if (error) { 13 | logger.logError('API', 'GET /api/auth/mastodon', error, undefined, { message: 'Failed to fetch Mastodon instances' }) 14 | return NextResponse.json( 15 | { success: false, error: 'Failed to fetch Mastodon instances' }, 16 | { status: 500 } 17 | ) 18 | } 19 | 20 | // Transformation des données pour n'avoir que la liste des instances 21 | const instances = data.map(item => item.instance) 22 | return NextResponse.json({ 23 | success: true, 24 | instances: instances 25 | }) 26 | 27 | } catch (error) { 28 | logger.logError('API', 'GET /api/auth/mastodon', error, undefined, { message: 'An unexpected error occurred' }) 29 | return NextResponse.json( 30 | { success: false, error: 'An unexpected error occurred' }, 31 | { status: 500 } 32 | ) 33 | } 34 | } 35 | 36 | // Exporter la fonction GET enveloppée par le middleware de logging 37 | export const GET = withLogging(mastodonHandler) -------------------------------------------------------------------------------- /src/app/api/auth/refresh/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { auth } from "@/app/auth" 3 | import { AccountService } from "@/lib/services/accountService" 4 | import { unlinkAccount } from "@/lib/supabase-adapter" 5 | import logger, { withLogging } from '@/lib/log_utils' 6 | 7 | async function refreshHandler(request: Request) { 8 | try { 9 | const session = await auth() 10 | if (!session?.user?.id) { 11 | logger.logWarning('API', 'POST /api/auth/refresh', 'Unauthorized access attempt') 12 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) 13 | } 14 | 15 | const accountService = new AccountService() 16 | const results: { bluesky?: any; mastodon?: any } = {} 17 | const invalidProviders: string[] = [] 18 | 19 | // Vérifier Bluesky si l'utilisateur a un compte 20 | if (session.user.bluesky_username) { 21 | results.bluesky = await accountService.verifyAndRefreshBlueskyToken(session.user.id) 22 | if (results.bluesky.requiresReauth) { 23 | invalidProviders.push('bluesky') 24 | } 25 | } 26 | 27 | // Vérifier Mastodon si l'utilisateur a un compte 28 | if (session.user.mastodon_username) { 29 | results.mastodon = await accountService.verifyAndRefreshMastodonToken(session.user.id) 30 | if (results.mastodon.requiresReauth) { 31 | invalidProviders.push('mastodon') 32 | } 33 | } 34 | 35 | // Si aucun compte n'est configuré 36 | if (!session.user.bluesky_username && !session.user.mastodon_username) { 37 | return NextResponse.json({ 38 | success: false, 39 | error: 'No social accounts configured' 40 | }) 41 | } 42 | 43 | // Si des providers nécessitent une réauthentification 44 | if (invalidProviders.length > 0) { 45 | logger.logWarning('API', 'POST /api/auth/refresh', 'Reauth required for providers', session.user.id, { providers: invalidProviders }) 46 | return NextResponse.json( 47 | { 48 | success: false, 49 | error: 'Token refresh failed', 50 | providers: invalidProviders, 51 | // ...results 52 | }, 53 | // { status: 401 } 54 | ) 55 | } 56 | 57 | return NextResponse.json({ 58 | success: true, 59 | ...results 60 | }) 61 | } catch (error) { 62 | logger.logError('API', 'POST /api/auth/refresh', error) 63 | return NextResponse.json( 64 | { error: 'Internal server error' }, 65 | { status: 500 } 66 | ) 67 | } 68 | } 69 | 70 | export const POST = withLogging(refreshHandler) -------------------------------------------------------------------------------- /src/app/api/auth/unlink/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { auth } from "@/app/auth" 3 | import { supabaseAdapter, UnlinkError } from "@/lib/supabase-adapter" 4 | import logger, { withLogging } from '@/lib/log_utils' 5 | 6 | async function unlinkHandler(req: Request) { 7 | try { 8 | const session = await auth() 9 | if (!session?.user?.id) { 10 | logger.logWarning('API', 'POST /api/auth/unlink', 'Unauthorized access attempt') 11 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) 12 | } 13 | const { provider } = await req.json() 14 | if (!['twitter', 'bluesky', 'mastodon', 'piaille'].includes(provider)) { 15 | logger.logWarning('API', 'POST /api/auth/unlink', 'Invalid provider requested', session.user.id, { provider }) 16 | return NextResponse.json({ error: 'Invalid provider' }, { status: 400 }) 17 | } 18 | if (!supabaseAdapter.unlinkAccount || !supabaseAdapter.getAccountsByUserId) { 19 | logger.logError('API', 'POST /api/auth/unlink', new Error('Required adapter methods are not implemented'), session.user.id) 20 | throw new Error('Required adapter methods are not implemented') 21 | } 22 | const accounts = await supabaseAdapter.getAccountsByUserId(session.user.id) 23 | 24 | // Find the account to unlink 25 | const accountToUnlink = accounts.find(account => account.provider === provider) 26 | if (!accountToUnlink) { 27 | logger.logWarning('API', 'POST /api/auth/unlink', 'Account not found for provider', session.user.id, { provider }) 28 | return NextResponse.json({ 29 | error: 'Account not found', 30 | code: 'NOT_LINKED' 31 | }, { status: 400 }) 32 | } 33 | await supabaseAdapter.unlinkAccount({ 34 | provider: accountToUnlink.provider, 35 | providerAccountId: accountToUnlink.providerAccountId 36 | }) 37 | return NextResponse.json({ success: true }) 38 | } catch (error) { 39 | const userId = (await auth())?.user?.id || 'unknown' 40 | logger.logError('API', 'POST /api/auth/unlink', error, userId, { 41 | name: error.name, 42 | message: error.message 43 | }) 44 | 45 | if (error instanceof UnlinkError) { 46 | return NextResponse.json( 47 | { error: error.message, code: error.code }, 48 | { status: error.status } 49 | ) 50 | } 51 | 52 | return NextResponse.json( 53 | { error: 'Internal server error', code: 'UNKNOWN_ERROR' }, 54 | { status: 500 } 55 | ) 56 | } 57 | } 58 | 59 | export const POST = withLogging(unlinkHandler) -------------------------------------------------------------------------------- /src/app/api/import-status/[jobId]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { supabase } from '@/lib/supabase' 3 | import { auth } from "@/app/auth"; 4 | import logger, { withLogging } from '@/lib/log_utils'; 5 | 6 | async function getImportStatus(request: NextRequest) { 7 | try { 8 | // Récupérer et attendre les paramètres 9 | const jobId = request.nextUrl.pathname.split('/').pop(); 10 | 11 | const session = await auth(); 12 | 13 | if (!session?.user?.id) { 14 | logger.logWarning('API', 'GET /api/import-status/[jobId]', 'Unauthorized access attempt'); 15 | return NextResponse.json( 16 | { error: "Not authenticated" }, 17 | { status: 401 } 18 | ); 19 | } 20 | 21 | // Récupérer le statut du job 22 | const { data: job, error: jobError } = await supabase 23 | .from('import_jobs') 24 | .select('*') 25 | .eq('id', jobId) 26 | .eq('user_id', session.user.id) 27 | .single(); 28 | 29 | if (jobError) { 30 | logger.logError('API', 'GET /api/import-status/[jobId]', new Error(jobError.message), session.user.id, { 31 | jobId, 32 | context: 'Database query error' 33 | }); 34 | return NextResponse.json( 35 | { error: 'Failed to fetch job status' }, 36 | { status: 500 } 37 | ); 38 | } 39 | 40 | if (!job) { 41 | logger.logWarning('API', 'GET /api/import-status/[jobId]', 'Job not found', session.user.id, { jobId }); 42 | return NextResponse.json( 43 | { error: 'Job not found' }, 44 | { status: 404 } 45 | ); 46 | } 47 | 48 | const stats = job.stats || { 49 | followers: 0, 50 | following: 0, 51 | total: job.total_items || 0, 52 | processed: 0 53 | }; 54 | 55 | logger.logInfo('API', 'GET /api/import-status/[jobId]', 'Job status retrieved', session.user.id, { 56 | jobId: job.id, 57 | status: job.status, 58 | progress: `${stats.processed}/${stats.total} items`, 59 | totalItems: stats.total, 60 | stats 61 | }); 62 | 63 | return NextResponse.json({ 64 | jobId: job.id, 65 | status: job.status, 66 | progress: stats.processed, 67 | totalItems: stats.total, 68 | stats, 69 | error: job.error_log 70 | }); 71 | 72 | } catch (error) { 73 | const userId = (await auth())?.user?.id || 'unknown'; 74 | logger.logError('API', 'GET /api/import-status/[jobId]', error, userId, { 75 | context: 'Unexpected error in import status check' 76 | }); 77 | return NextResponse.json( 78 | { error: 'Failed to check import status' }, 79 | { status: 500 } 80 | ); 81 | } 82 | } 83 | 84 | export const GET = withLogging(getImportStatus); -------------------------------------------------------------------------------- /src/app/api/migrate/matching_found/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { auth } from '@/app/auth'; 3 | import { MatchingService } from '@/lib/services/matchingService'; 4 | import logger, { withLogging } from '@/lib/log_utils'; 5 | 6 | async function matchingFoundHandler() { 7 | try { 8 | const session = await auth(); 9 | 10 | if (!session?.user?.id) { 11 | logger.logWarning('API', 'GET /api/migrate/matching_found', 'Unauthorized access attempt'); 12 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 13 | } 14 | 15 | const matchingService = new MatchingService(); 16 | let result; 17 | 18 | if (!session.user?.has_onboarded) { 19 | if (!session?.user?.twitter_id) { 20 | logger.logWarning('API', 'GET /api/migrate/matching_found', 'Twitter ID not found in session', session.user.id); 21 | return NextResponse.json( 22 | { error: 'Twitter ID not found in session' }, 23 | { status: 400 } 24 | ); 25 | } 26 | result = await matchingService.getSourcesFromFollower(session.user.twitter_id); 27 | } else { 28 | result = await matchingService.getFollowableTargets(session.user.id); 29 | } 30 | return NextResponse.json({ matches: result }); 31 | 32 | } catch (error) { 33 | const userId = (await auth())?.user?.id || 'unknown'; 34 | logger.logError('API', 'GET /api/migrate/matching_found', error, userId, { 35 | context: 'Error in matching_found route' 36 | }); 37 | return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); 38 | } 39 | } 40 | 41 | export const GET = withLogging(matchingFoundHandler); -------------------------------------------------------------------------------- /src/app/api/share/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { auth } from "@/app/auth"; 3 | import { UserService } from '@/lib/services/userServices'; 4 | import { UserRepository } from '@/lib/repositories/userRepository'; 5 | 6 | export async function POST(request: Request) { 7 | const session = await auth(); 8 | if (!session?.user?.id) { 9 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 10 | } 11 | 12 | try { 13 | const body = await request.json(); 14 | const shareService = new UserService(); 15 | await shareService.recordShareEvent(session.user.id, body.platform, body.success); 16 | return NextResponse.json({ success: true }); 17 | } catch (error) { 18 | console.error('Share error:', error); 19 | return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); 20 | } 21 | } 22 | 23 | export async function GET(request: Request) { 24 | const session = await auth(); 25 | if (!session?.user?.id) { 26 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 27 | } 28 | 29 | try { 30 | const userRepo = new UserRepository(); 31 | const hasShares = await userRepo.hasShareEvents(session.user.id); 32 | 33 | console.log(`User ${session.user.id} has shares: ${hasShares}`); 34 | return NextResponse.json({ hasShares }); 35 | } catch (error) { 36 | console.error('Share check error:', error); 37 | return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); 38 | } 39 | } -------------------------------------------------------------------------------- /src/app/api/stats/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { auth } from '@/app/auth'; 3 | import { StatsService } from '@/lib/services/statsServices'; 4 | import { StatsRepository } from '@/lib/repositories/statsRepository'; 5 | import logger, { withLogging } from '@/lib/log_utils'; 6 | 7 | async function userStatsHandler() { 8 | try { 9 | const session = await auth(); 10 | if (!session?.user?.id) { 11 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 12 | } 13 | 14 | if (!session?.user?.has_onboarded) { 15 | if (!session?.user?.twitter_id) { 16 | return NextResponse.json({ 17 | connections: { 18 | followers: 0, 19 | following: 0 20 | }, 21 | matches: { 22 | bluesky: { total: 0, hasFollowed: 0, notFollowed: 0 }, 23 | mastodon: { total: 0, hasFollowed: 0, notFollowed: 0 } 24 | } 25 | }); 26 | } 27 | 28 | } 29 | 30 | const repository = new StatsRepository(); 31 | const statsService = new StatsService(repository); 32 | 33 | const stats = await statsService.getUserStats(session.user.id, session.user.has_onboarded); 34 | return NextResponse.json(stats); 35 | } catch (error) { 36 | const userId = (await auth())?.user?.id || 'unknown'; 37 | logger.logError('API', 'GET /api/stats', error, userId, { 38 | context: 'Retrieving user stats' 39 | }); 40 | return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); 41 | } 42 | } 43 | 44 | export const GET = withLogging(userStatsHandler); -------------------------------------------------------------------------------- /src/app/api/stats/total/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { auth } from '@/app/auth'; 3 | import { StatsService } from '@/lib/services/statsServices'; 4 | import { StatsRepository } from '@/lib/repositories/statsRepository'; 5 | import logger, { withLogging } from '@/lib/log_utils'; 6 | 7 | async function globalStatsHandler() { 8 | try { 9 | const session = await auth(); 10 | if (!session) { 11 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 12 | } 13 | 14 | const repository = new StatsRepository(); 15 | const statsService = new StatsService(repository); 16 | 17 | const stats = await statsService.getGlobalStats(); 18 | 19 | return NextResponse.json(stats); 20 | } catch (error) { 21 | const userId = (await auth())?.user?.id || 'unknown'; 22 | logger.logError('API', 'GET /api/stats/total', error, userId, { 23 | context: 'Retrieving global stats' 24 | }); 25 | return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); 26 | } 27 | } 28 | 29 | export const GET = withLogging(globalStatsHandler); -------------------------------------------------------------------------------- /src/app/api/support/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import nodemailer from 'nodemailer'; 3 | import { auth } from '@/app/auth'; 4 | import logger, { withLogging } from '@/lib/log_utils'; 5 | 6 | const transporter = nodemailer.createTransport({ 7 | host: process.env.SMTP_HOST, 8 | port: parseInt(process.env.SMTP_PORT || "587"), 9 | secure: process.env.SMTP_PORT === "465", 10 | auth: { 11 | user: process.env.EMAIL_USER, 12 | pass: process.env.EMAIL_PASS 13 | } 14 | }); 15 | 16 | async function supportHandler(request: Request) { 17 | try { 18 | const session = await auth(); 19 | const { subject, message, email } = await request.json(); 20 | 21 | // Construire le sujet avec le statut d'authentification 22 | const authStatus = session?.user 23 | ? `[Auth - ID: ${session.user.id}]` 24 | : '[Non Auth]'; 25 | 26 | const mailOptions = { 27 | from: process.env.EMAIL_USER, 28 | to: process.env.EMAIL_USER, 29 | subject: `Support ${authStatus}: ${subject}`, 30 | replyTo: email, // Add reply-to field with client's email 31 | text: message, 32 | html: ` 33 |

Nouveau message de support

34 | ${session?.user ? `

Utilisateur ID: ${session.user.id}

` : '

Utilisateur: Non authentifié

'} 35 |

Email: ${email}

36 |

Sujet: ${subject}

37 |

Message:

38 |

${message.replace(/\n/g, '
')}

39 | ` 40 | }; 41 | 42 | await transporter.sendMail(mailOptions); 43 | 44 | return NextResponse.json({ success: true }); 45 | } catch (error) { 46 | const userId = (await auth())?.user?.id || 'unknown'; 47 | logger.logError('API', 'POST /api/support', error, userId, { 48 | context: 'Sending support email' 49 | }); 50 | return NextResponse.json( 51 | { error: 'Failed to send email' }, 52 | { status: 500 } 53 | ); 54 | } 55 | } 56 | 57 | export const POST = withLogging(supportHandler); -------------------------------------------------------------------------------- /src/app/api/tasks/[taskId]/route.ts: -------------------------------------------------------------------------------- 1 | // src/app/api/tasks/[taskId]/route.ts 2 | import { NextRequest, NextResponse } from 'next/server'; 3 | import { auth } from '@/app/auth'; 4 | import { createClient } from '@supabase/supabase-js'; 5 | 6 | export async function GET( 7 | request: NextRequest, 8 | { params }: { params: { taskId: string } } 9 | ) { 10 | try { 11 | const session = await auth(); 12 | 13 | if (!session?.user?.id) { 14 | return NextResponse.json( 15 | { success: false, error: 'Unauthorized' }, 16 | { status: 401 } 17 | ); 18 | } 19 | 20 | const paramsData = await params; 21 | const taskId = paramsData.taskId; 22 | 23 | if (!taskId) { 24 | return NextResponse.json( 25 | { success: false, error: 'Task ID is required' }, 26 | { status: 400 } 27 | ); 28 | } 29 | 30 | // Créer une connexion à Supabase 31 | const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; 32 | const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!; 33 | const supabase = createClient(supabaseUrl, supabaseKey); 34 | 35 | // Récupérer la tâche par son ID 36 | const { data, error } = await supabase 37 | .from('python_tasks') 38 | .select('*') 39 | .eq('id', taskId) 40 | .single(); 41 | 42 | if (error) { 43 | console.error('API', 'GET /api/tasks/[taskId]', error); 44 | return NextResponse.json( 45 | { success: false, error: 'Task not found' }, 46 | { status: 404 } 47 | ); 48 | } 49 | 50 | // Vérifier que l'utilisateur est autorisé à accéder à cette tâche 51 | if (data.user_id !== session.user.id) { 52 | return NextResponse.json( 53 | { success: false, error: 'Unauthorized: Task belongs to another user' }, 54 | { status: 403 } 55 | ); 56 | } 57 | 58 | return NextResponse.json({ 59 | success: true, 60 | task: data 61 | }); 62 | 63 | } catch (error) { 64 | console.error('API', 'GET /api/tasks/[taskId]', error); 65 | return NextResponse.json( 66 | { 67 | success: false, 68 | error: 'Internal server error', 69 | details: error instanceof Error ? error.message : String(error) 70 | }, 71 | { status: 500 } 72 | ); 73 | } 74 | } -------------------------------------------------------------------------------- /src/app/api/update/user_stats/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { StatsRepository } from '@/lib/repositories/statsRepository'; 3 | import { StatsService } from '@/lib/services/statsServices'; 4 | import { auth } from '@/app/auth'; 5 | import logger, { withLogging } from '@/lib/log_utils'; 6 | 7 | async function updateUserStatsHandler() { 8 | try { 9 | const session = await auth(); 10 | if (!session?.user?.id) { 11 | logger.logWarning('API', 'POST /api/update/user_stats', 'Unauthorized request - no user ID found in session'); 12 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 13 | } 14 | const statsRepository = new StatsRepository(); 15 | const statsService = new StatsService(statsRepository); 16 | 17 | await statsService.refreshUserStats(session.user.id, session.user.has_onboarded); 18 | 19 | return NextResponse.json({ success: true }); 20 | } catch (error) { 21 | console.error('[UserStats] Error updating user stats:', error); 22 | if (error instanceof Error) { 23 | console.error('[UserStats] Error details:', error.message); 24 | } 25 | const userId = (await auth())?.user?.id || 'unknown'; 26 | logger.logError('API', 'POST /api/update/user_stats', error, userId, { 27 | context: 'Updating user stats' 28 | }); 29 | return NextResponse.json( 30 | { error: 'Failed to update user stats' }, 31 | { status: 500 } 32 | ); 33 | } 34 | } 35 | 36 | export const POST = withLogging(updateUserStatsHandler); -------------------------------------------------------------------------------- /src/app/api/users/automatic-reconnect/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { auth } from "@/app/auth"; 3 | import { authClient } from '@/lib/supabase' 4 | import logger, { withLogging } from '@/lib/log_utils'; 5 | 6 | async function automaticReconnectHandler(request: Request) { 7 | try { 8 | const session = await auth(); 9 | 10 | if (!session?.user?.id) { 11 | logger.logWarning('API', 'POST /api/users/automatic-reconnect', 'Unauthorize','anonymous'); 12 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 13 | } 14 | 15 | const { automatic_reconnect } = await request.json(); 16 | 17 | if (typeof automatic_reconnect !== 'boolean') { 18 | logger.logWarning('API', 'POST /api/users/automatic-reconnect', 'Invalid value for automatic_reconnect', session.user.id); 19 | return NextResponse.json({ error: 'Invalid value for automatic_reconnect' }, { status: 400 }); 20 | } 21 | 22 | // Mettre à jour dans Supabase 23 | const { error: updateError } = await authClient 24 | .from('users') 25 | .update({ automatic_reconnect }) 26 | .eq('id', session.user.id); 27 | 28 | if (updateError) { 29 | logger.logError('API', 'POST /api/users/automatic-reconnect', updateError, session.user.id, { 30 | context: 'Updating automatic_reconnect setting' 31 | }); 32 | return NextResponse.json({ error: 'Failed to update automatic_reconnect' }, { status: 500 }); 33 | } 34 | 35 | return NextResponse.json({ success: true, automatic_reconnect }); 36 | 37 | } catch (error) { 38 | const userId = (await auth())?.user?.id || 'unknown'; 39 | logger.logError('API', 'POST /api/users/automatic-reconnect', error, userId, { 40 | context: 'Processing automatic reconnect request' 41 | }); 42 | return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); 43 | } 44 | } 45 | 46 | export const POST = withLogging(automaticReconnectHandler); -------------------------------------------------------------------------------- /src/app/api/users/bot-newsletter/route.ts: -------------------------------------------------------------------------------- 1 | // import { NextResponse } from 'next/server'; 2 | // import { auth } from "@/app/auth"; 3 | // import { authClient } from '@/lib/supabase' 4 | // import logger, { withLogging } from '@/lib/log_utils'; 5 | 6 | // /** 7 | // * Gère les requêtes POST pour mettre à jour le statut have_seen_bot_newsletter de l'utilisateur 8 | // */ 9 | // async function botNewsletterHandler(request: Request) { 10 | // try { 11 | // const session = await auth(); 12 | 13 | // if (!session?.user?.id) { 14 | // logger.logWarning('API', 'POST /api/users/bot-newsletter', 'Unauthorized', 'anonymous'); 15 | // return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 16 | // } 17 | 18 | // const { haveSeenBotNewsletter } = await request.json(); 19 | 20 | // if (typeof haveSeenBotNewsletter !== 'boolean') { 21 | // logger.logWarning('API', 'POST /api/users/bot-newsletter', 'Invalid value for haveSeenBotNewsletter', session.user.id); 22 | // return NextResponse.json({ error: 'Invalid value for haveSeenBotNewsletter' }, { status: 400 }); 23 | // } 24 | 25 | // // // Mettre à jour dans Supabase 26 | // // const { error: updateError } = await authClient 27 | // // .from('users') 28 | // // .update({ have_seen_bot_newsletter: haveSeenBotNewsletter }) 29 | // // .eq('id', session.user.id); 30 | 31 | // // if (updateError) { 32 | // // logger.logError('API', 'POST /api/users/bot-newsletter', updateError, session.user.id, { 33 | // // context: 'Updating have_seen_bot_newsletter setting' 34 | // // }); 35 | // // return NextResponse.json({ error: 'Failed to update have_seen_bot_newsletter' }, { status: 500 }); 36 | // // } 37 | 38 | // return NextResponse.json({ success: true, have_seen_bot_newsletter: haveSeenBotNewsletter }); 39 | 40 | // } catch (error) { 41 | // const userId = (await auth())?.user?.id || 'unknown'; 42 | // logger.logError('API', 'POST /api/users/bot-newsletter', error, userId, { 43 | // context: 'Processing bot newsletter request' 44 | // }); 45 | // return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); 46 | // } 47 | // } 48 | 49 | // export const POST = withLogging(botNewsletterHandler); -------------------------------------------------------------------------------- /src/app/api/users/language/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { auth } from "@/app/auth" 3 | import { UserService } from '@/lib/services/userServices'; 4 | 5 | const userService = new UserService(); 6 | 7 | export async function GET(req: NextRequest) { 8 | try { 9 | const session = await auth() 10 | if (!session?.user?.id) { 11 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 12 | } 13 | 14 | const languagePref = await userService.getLanguagePreference(session.user.id); 15 | return NextResponse.json(languagePref); 16 | } catch (error) { 17 | console.error('Error getting language preference:', error); 18 | return NextResponse.json( 19 | { error: 'Failed to get language preference' }, 20 | { status: 500 } 21 | ); 22 | } 23 | } 24 | 25 | export async function POST(req: NextRequest) { 26 | try { 27 | const session = await auth() 28 | if (!session?.user?.id) { 29 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 30 | } 31 | 32 | const data = await req.json(); 33 | const { language } = data; 34 | 35 | if (!language) { 36 | return NextResponse.json( 37 | { error: 'Language is required' }, 38 | { status: 400 } 39 | ); 40 | } 41 | 42 | // const metadata = { 43 | // ip_address: req.ip, 44 | // user_agent: req.headers.get('user-agent') 45 | // }; 46 | 47 | await userService.updateLanguagePreference(session.user.id, language); 48 | return NextResponse.json({ success: true }); 49 | } catch (error) { 50 | console.error('Error updating language preference:', error); 51 | return NextResponse.json( 52 | { error: 'Failed to update language preference' }, 53 | { status: 500 } 54 | ); 55 | } 56 | } -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ISCPIF/OpenPortability/e2ab74e39c25954a6387d06252854863e89ff19b/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ISCPIF/OpenPortability/e2ab74e39c25954a6387d06252854863e89ff19b/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ISCPIF/OpenPortability/e2ab74e39c25954a6387d06252854863e89ff19b/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /src/app/fonts/SyneTactile-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ISCPIF/OpenPortability/e2ab74e39c25954a6387d06252854863e89ff19b/src/app/fonts/SyneTactile-Regular.ttf -------------------------------------------------------------------------------- /src/app/fonts/plex.ts: -------------------------------------------------------------------------------- 1 | import { IBM_Plex_Mono, Caveat } from 'next/font/google' 2 | 3 | export const plex = IBM_Plex_Mono({ 4 | subsets: ['latin'], 5 | weight: ["300", "400", "700"] 6 | }) 7 | 8 | export const caveat = Caveat({ 9 | subsets: ['latin'], 10 | weight: ["400", "700"] 11 | }) 12 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @font-face { 4 | font-family: 'SyneTactile'; 5 | src: url('/fonts/SyneTactile-Regular.ttf') format('truetype'); 6 | font-weight: normal; 7 | font-style: normal; 8 | } 9 | 10 | :root { 11 | --background: #ffffff; 12 | --foreground: #171717; 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | --font-space-grotesk: 'Space Grotesk', sans-serif; 17 | } 18 | 19 | @media (prefers-color-scheme: dark) { 20 | :root { 21 | --background: #0a0a0a; 22 | --foreground: #ededed; 23 | } 24 | } 25 | 26 | body { 27 | color: var(--foreground); 28 | background: var(--background); 29 | font-family: Arial, Helvetica, sans-serif; 30 | } 31 | 32 | .font-space-grotesk { 33 | font-family: var(--font-space-grotesk); 34 | } 35 | 36 | @keyframes blob { 37 | 0% { 38 | transform: translate(0px, 0px) scale(1); 39 | } 40 | 33% { 41 | transform: translate(30px, -50px) scale(1.1); 42 | } 43 | 66% { 44 | transform: translate(-20px, 20px) scale(0.9); 45 | } 46 | 100% { 47 | transform: translate(0px, 0px) scale(1); 48 | } 49 | } 50 | 51 | .animate-blob { 52 | animation: blob 7s infinite; 53 | } 54 | 55 | .animation-delay-2000 { 56 | animation-delay: 2s; 57 | } 58 | 59 | .animation-delay-4000 { 60 | animation-delay: 4s; 61 | } 62 | 63 | /* Animation de mise en évidence pour la section de support personnalisé */ 64 | @keyframes highlight-pulse { 65 | 0% { 66 | box-shadow: 0 0 0 0 rgba(214, 53, 111, 0.7); 67 | } 68 | 70% { 69 | box-shadow: 0 0 0 10px rgba(214, 53, 111, 0); 70 | } 71 | 100% { 72 | box-shadow: 0 0 0 0 rgba(214, 53, 111, 0); 73 | } 74 | } 75 | 76 | .highlight-animation { 77 | animation: highlight-pulse 1s ease-in-out 3; 78 | } 79 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function RootLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode 5 | }) { 6 | // Remove console.log as it can also contribute to hydration issues 7 | // by executing differently on server vs client 8 | 9 | // Don't wrap with HTML or body tags - these are already in the locale layout 10 | return children; 11 | } -------------------------------------------------------------------------------- /src/app/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { SessionProvider } from "next-auth/react" 4 | 5 | type ProvidersProps = { 6 | children: React.ReactNode; 7 | session?: any; 8 | } 9 | 10 | export function Providers({ children, session }: ProvidersProps) { 11 | // console.log("Session provider called !", { session }) 12 | return ( 13 | 14 | {children} 15 | 16 | ) 17 | } -------------------------------------------------------------------------------- /src/hooks/useAuthTokens.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState, useRef } from "react"; 2 | 3 | // Module-level variables to track verification state across component instances 4 | const globalTokensVerified = { current: false }; 5 | // Shared promise for concurrent verification requests 6 | let activeVerificationPromise: Promise | null = null; 7 | 8 | export function useAuthTokens() { 9 | const [missingProviders, setMissingProviders] = useState<('bluesky' | 'mastodon')[]>([]); 10 | // Local ref to track if tokens have been verified in this component instance 11 | const tokensVerifiedRef = useRef(globalTokensVerified.current); 12 | 13 | const verifyTokens = useCallback(async () => { 14 | // If tokens are already verified, return immediately 15 | if (tokensVerifiedRef.current || globalTokensVerified.current) { 16 | tokensVerifiedRef.current = true; 17 | globalTokensVerified.current = true; 18 | return { isValid: true }; 19 | } 20 | 21 | // If there's an active verification in progress, reuse that promise 22 | if (activeVerificationPromise) { 23 | return activeVerificationPromise; 24 | } 25 | 26 | // Create a new verification promise 27 | activeVerificationPromise = (async () => { 28 | try { 29 | const response = await fetch('/api/auth/refresh', { 30 | method: 'POST', 31 | headers: { 32 | 'Cache-Control': 'no-cache', 33 | 'X-Request-ID': `auth-refresh-${Date.now()}` // Add unique identifier 34 | } 35 | }); 36 | const data = await response.json(); 37 | 38 | if (!data.success && data.providers) { 39 | setMissingProviders(data.providers); 40 | } 41 | 42 | tokensVerifiedRef.current = true; 43 | globalTokensVerified.current = true; 44 | 45 | return { isValid: data.success, providers: data.providers }; 46 | } catch (error) { 47 | console.error('Error verifying tokens:', error); 48 | return { isValid: false, error }; 49 | } finally { 50 | // Clear the active promise reference when done 51 | activeVerificationPromise = null; 52 | } 53 | })(); 54 | 55 | return activeVerificationPromise; 56 | }, []); 57 | 58 | // Function to manually reset the verification state (useful for logout, etc.) 59 | const resetTokenVerification = useCallback(() => { 60 | tokensVerifiedRef.current = false; 61 | globalTokensVerified.current = false; 62 | activeVerificationPromise = null; 63 | }, []); 64 | 65 | return { missingProviders, verifyTokens, resetTokenVerification }; 66 | } -------------------------------------------------------------------------------- /src/hooks/useMastodonInstances.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export function useMastodonInstances() { 4 | const [mastodonInstances, setMastodonInstances] = useState([]); 5 | 6 | useEffect(() => { 7 | const fetchMastodonInstances = async () => { 8 | try { 9 | const response = await fetch('/api/auth/mastodon'); 10 | const data = await response.json(); 11 | if (data.success) { 12 | setMastodonInstances(data.instances); 13 | } 14 | } catch (error) { 15 | console.error('Error fetching Mastodon instances:', error); 16 | } 17 | }; 18 | 19 | fetchMastodonInstances(); 20 | }, []); 21 | 22 | return mastodonInstances; 23 | } -------------------------------------------------------------------------------- /src/i18n/navigation.ts: -------------------------------------------------------------------------------- 1 | import {createSharedPathnamesNavigation} from 'next-intl/navigation'; 2 | 3 | export const locales = ['en', 'fr', 'es', 'it', 'de', 'sv', 'pt'] as const; 4 | export const localePrefix = 'always'; // Default 5 | 6 | export const {Link, redirect, usePathname, useRouter} = 7 | createSharedPathnamesNavigation({locales, localePrefix}); -------------------------------------------------------------------------------- /src/i18n/request.ts: -------------------------------------------------------------------------------- 1 | import {getRequestConfig} from 'next-intl/server'; 2 | import { headers } from 'next/headers'; 3 | 4 | export default getRequestConfig(async () => { 5 | const headersList = await headers(); 6 | const locale = headersList.get('X-NEXT-INTL-LOCALE') || 'fr'; 7 | return { 8 | messages: (await import(`../../messages/${locale}.json`)).default, 9 | locale: locale 10 | }; 11 | }); -------------------------------------------------------------------------------- /src/lib/bluesky.ts: -------------------------------------------------------------------------------- 1 | import { BskyAgent } from '@atproto/api' 2 | 3 | const BSKY_SERVICE = 'https://bsky.social' 4 | 5 | type BlueSkySession = { 6 | accessToken: string 7 | refreshToken: string 8 | handle: string 9 | } 10 | 11 | export async function getProfileInfo(handle: string, session?: BlueSkySession) { 12 | const agent = new BskyAgent({ service: BSKY_SERVICE }) 13 | 14 | try { 15 | if (session?.accessToken) { 16 | await agent.resumeSession({ 17 | accessJwt: session.accessToken, 18 | refreshJwt: session.refreshToken, 19 | handle: session.handle, 20 | did: `did:plc:${session.handle.replace('.', '')}`, 21 | active: true 22 | }) 23 | } 24 | 25 | const response = await agent.getProfile({ actor: handle }) 26 | return { 27 | handle: response.data.handle, 28 | displayName: response.data.displayName, 29 | avatar: response.data.avatar, 30 | description: response.data.description 31 | } 32 | } catch (error) { 33 | console.error(`Erreur lors de la récupération du profil ${handle}:`, error) 34 | return null 35 | } 36 | } -------------------------------------------------------------------------------- /src/lib/dataFormating.ts: -------------------------------------------------------------------------------- 1 | import { UserCompleteStats } from "@/lib/types/stats"; 2 | 3 | export function formatNumber(num: number): string { 4 | return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' '); 5 | } 6 | 7 | export function calculateTotalMatches(stats: UserCompleteStats) { 8 | const totalMatches = stats.matches.bluesky.notFollowed + stats.matches.mastodon.notFollowed; 9 | const totalHasFollowed = stats.matches.bluesky.hasFollowed + stats.matches.mastodon.hasFollowed; 10 | 11 | return { totalMatches, totalHasFollowed }; 12 | } -------------------------------------------------------------------------------- /src/lib/encryption.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from 'crypto-js'; 2 | 3 | const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || ''; 4 | 5 | if (!ENCRYPTION_KEY) { 6 | throw new Error('ENCRYPTION_KEY environment variable is not set'); 7 | } 8 | 9 | export function encrypt(text: string): string { 10 | if (!text) return text; 11 | return CryptoJS.AES.encrypt(text, ENCRYPTION_KEY).toString(); 12 | } 13 | 14 | export function decrypt(encryptedText: string): string { 15 | if (!encryptedText) return encryptedText; 16 | const bytes = CryptoJS.AES.decrypt(encryptedText, ENCRYPTION_KEY); 17 | return bytes.toString(CryptoJS.enc.Utf8); 18 | } -------------------------------------------------------------------------------- /src/lib/services/newsletterService.ts: -------------------------------------------------------------------------------- 1 | // Types for newsletter data matching DB structure 2 | export type ConsentType = 'email_newsletter' | 'oep_newsletter' | 'research_participation' | 'personalized_support' | 'bluesky_dm' | 'mastodon_dm'; 3 | 4 | export interface Consent { 5 | type: ConsentType; 6 | value: boolean; 7 | } 8 | 9 | // Type pour les données brutes reçues de l'API 10 | export interface RawNewsletterResponse { 11 | email?: string; 12 | email_newsletter?: boolean; 13 | oep_newsletter?: boolean; 14 | research_participation?: boolean; 15 | personalized_support?: boolean; 16 | bluesky_dm?: boolean; 17 | mastodon_dm?: boolean; 18 | } 19 | 20 | export interface NewsletterData { 21 | email?: string; 22 | consents: { 23 | [key in ConsentType]?: boolean; 24 | }; 25 | } 26 | 27 | /** 28 | * Fetches all newsletter data 29 | */ 30 | export const fetchNewsletterData = async (): Promise => { 31 | const response = await fetch('/api/newsletter/request', { 32 | method: 'GET', 33 | headers: { 'Content-Type': 'application/json' } 34 | }); 35 | 36 | if (!response.ok) { 37 | throw new Error('Failed to fetch newsletter data'); 38 | } 39 | 40 | return response.json(); 41 | }; 42 | 43 | /** 44 | * Updates a single newsletter consent 45 | */ 46 | export const updateNewsletterConsent = async ( 47 | consent: Consent, 48 | email?: string 49 | ): Promise => { 50 | try { 51 | const response = await fetch('/api/newsletter/request', { 52 | method: 'POST', 53 | headers: { 'Content-Type': 'application/json' }, 54 | body: JSON.stringify({ 55 | email, 56 | consents: [consent] 57 | }) 58 | }); 59 | 60 | if (!response.ok) { 61 | throw new Error('Failed to update newsletter consent'); 62 | } 63 | 64 | await response.json(); 65 | return true; 66 | } catch (error) { 67 | console.error('Error updating newsletter consent:', error); 68 | return false; 69 | } 70 | }; -------------------------------------------------------------------------------- /src/lib/services/statsServices.ts: -------------------------------------------------------------------------------- 1 | import { StatsRepository } from "@/lib/repositories/statsRepository"; 2 | import { UserCompleteStats, GlobalStats, ReconnectionStats } from "@/lib/types/stats"; 3 | 4 | export class StatsService { 5 | private repository: StatsRepository; 6 | 7 | constructor(repository: StatsRepository) { 8 | this.repository = repository; 9 | } 10 | 11 | async getGlobalStats(): Promise { 12 | return this.repository.getGlobalStats(); 13 | } 14 | 15 | async getUserStats(userId: string, has_onboard: boolean): Promise { 16 | return this.repository.getUserCompleteStats(userId, has_onboard); 17 | } 18 | 19 | async getReconnectionStats(): Promise { 20 | const globalStats = await this.repository.getGlobalStats(); 21 | 22 | return { 23 | connections: globalStats.connections.followers + globalStats.connections.following, 24 | blueskyMappings: globalStats.connections.withHandle, 25 | sources: globalStats.users.total 26 | }; 27 | } 28 | 29 | async refreshUserStats(userId: string, has_onboard: boolean): Promise { 30 | return this.repository.refreshUserStatsCache(userId, has_onboard); 31 | } 32 | 33 | async refreshGlobalStats(): Promise { 34 | return this.repository.refreshGlobalStatsCache(); 35 | } 36 | } -------------------------------------------------------------------------------- /src/lib/supabase.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js' 2 | 3 | if (!process.env.NEXT_PUBLIC_SUPABASE_URL) { 4 | throw new Error('Missing env.NEXT_PUBLIC_SUPABASE_URL') 5 | } 6 | 7 | if (!process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) { 8 | throw new Error('Missing env.NEXT_PUBLIC_SUPABASE_ANON_KEY') 9 | } 10 | 11 | if (!process.env.SUPABASE_SERVICE_ROLE_KEY) { 12 | throw new Error('Missing env.SUPABASE_SERVICE_ROLE_KEY') 13 | } 14 | 15 | export const supabase = createClient( 16 | process.env.NEXT_PUBLIC_SUPABASE_URL, 17 | process.env.SUPABASE_SERVICE_ROLE_KEY, 18 | { 19 | auth: { 20 | autoRefreshToken: true, 21 | persistSession: true, 22 | detectSessionInUrl: true 23 | } 24 | } 25 | ) 26 | 27 | export const authClient = createClient( 28 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 29 | process.env.SUPABASE_SERVICE_ROLE_KEY!, 30 | { 31 | auth: { 32 | autoRefreshToken: false, 33 | persistSession: false 34 | }, 35 | db: { 36 | schema: "next-auth" 37 | } 38 | } 39 | ) -------------------------------------------------------------------------------- /src/lib/types/account.ts: -------------------------------------------------------------------------------- 1 | export type Provider = 'bluesky' | 'mastodon'; 2 | 3 | export interface TokenData { 4 | access_token: string; 5 | refresh_token?: string; 6 | expires_at?: Date; 7 | provider: Provider; 8 | provider_account_id?: string; 9 | } 10 | 11 | export interface TokenUpdate { 12 | access_token?: string; 13 | refresh_token?: string; 14 | expires_at?: Date; 15 | } 16 | 17 | export interface RefreshResult { 18 | success: boolean; 19 | error?: string; 20 | requiresReauth?: boolean; 21 | } 22 | 23 | export interface BlueskyCredentials { 24 | accessJwt: string; 25 | refreshJwt: string; 26 | handle: string; 27 | did: string; 28 | } -------------------------------------------------------------------------------- /src/lib/types/bluesky.ts: -------------------------------------------------------------------------------- 1 | export interface BlueskyProfile { 2 | did: string 3 | handle: string 4 | displayName?: string 5 | avatar?: string 6 | } 7 | 8 | export interface BlueskySessionData { 9 | accessJwt: string 10 | refreshJwt: string 11 | handle: string 12 | did: string 13 | } 14 | 15 | export interface BlueskyAuthResult { 16 | success: boolean 17 | data?: BlueskySessionData 18 | error?: string 19 | } 20 | 21 | export interface BatchFollowResult { 22 | attempted: number 23 | succeeded: number 24 | failures: Array<{ 25 | handle: string 26 | error: string 27 | }> 28 | } 29 | 30 | export interface IBlueskyRepository { 31 | getUserByBlueskyId(did: string): Promise 32 | linkBlueskyAccount(userId: string, blueskyData: BlueskySessionData): Promise 33 | updateBlueskyProfile(userId: string, profile: BlueskyProfile): Promise 34 | } 35 | 36 | export interface IBlueskyService { 37 | login(identifier: string, password: string): Promise 38 | resumeSession(sessionData: BlueskySessionData): Promise 39 | logout(): Promise 40 | getProfile(handle: string): Promise 41 | follow(did: string): Promise 42 | batchFollow(handles: string[]): Promise 43 | } -------------------------------------------------------------------------------- /src/lib/types/common.ts: -------------------------------------------------------------------------------- 1 | export interface UserSession { 2 | user: { 3 | id?: string; 4 | twitter_username?: string; 5 | bluesky_username?: string | null; 6 | mastodon_username?: string | null; 7 | mastodon_instance?: string | null; 8 | twitter_id?: string; 9 | bluesky_id?: string; 10 | mastodon_id?: string; 11 | has_onboarded?: boolean; 12 | have_seen_newsletter?: boolean; 13 | have_seen_bot_newsletter?: boolean; 14 | } 15 | } 16 | 17 | export interface ConnectedServices { 18 | twitter: boolean; 19 | bluesky: boolean; 20 | mastodon: boolean; 21 | } -------------------------------------------------------------------------------- /src/lib/types/mastodon.ts: -------------------------------------------------------------------------------- 1 | export interface MastodonCredentials { 2 | accessToken: string; 3 | userInstance: string; 4 | } 5 | 6 | export interface MastodonAccount { 7 | id: string; 8 | username: string; 9 | acct: string; 10 | url: string; 11 | } 12 | 13 | export interface MastodonFollowResult { 14 | success: boolean; 15 | error?: string; 16 | accountId?: string; 17 | } 18 | 19 | export interface MastodonTarget { 20 | username: string; 21 | instance: string; 22 | id?: string; 23 | } 24 | 25 | export interface MastodonBatchFollowResult { 26 | attempted: number; 27 | succeeded: number; 28 | failures: Array<{ 29 | handle: string; 30 | error?: string; 31 | }>; 32 | successfulHandles: string[]; 33 | } 34 | 35 | export interface IMastodonService { 36 | followAccount( 37 | accessToken: string, 38 | userInstance: string, 39 | targetUsername: string, 40 | targetInstance: string, 41 | targetId?: string 42 | ): Promise; 43 | 44 | batchFollow( 45 | accessToken: string, 46 | userInstance: string, 47 | targets: Array 48 | ): Promise; 49 | } -------------------------------------------------------------------------------- /src/lib/types/matching.ts: -------------------------------------------------------------------------------- 1 | export interface MatchingTarget { 2 | target_twitter_id: string | null; 3 | bluesky_handle: string | null; 4 | mastodon_handle: string | null; 5 | mastodon_username: string | null; 6 | mastodon_instance: string | null; 7 | mastodon_id: string | null; 8 | has_follow_bluesky: boolean; 9 | has_follow_mastodon: boolean; 10 | total_count?: number; 11 | } 12 | 13 | export interface MatchedFollower { 14 | source_twitter_id: string; 15 | bluesky_handle: string | null; 16 | mastodon_id: string | null; 17 | mastodon_username: string | null; 18 | mastodon_instance: string | null; 19 | has_been_followed_on_bluesky: boolean; 20 | has_been_followed_on_mastodon: boolean; 21 | full_count?: number; 22 | } 23 | 24 | export interface MatchingStats { 25 | total_following: number; 26 | matched_following: number; 27 | bluesky_matches: number; 28 | mastodon_matches: number; 29 | } 30 | 31 | export interface MatchingResult { 32 | following: MatchingTarget[]; 33 | stats: MatchingStats; 34 | } -------------------------------------------------------------------------------- /src/lib/types/stats.ts: -------------------------------------------------------------------------------- 1 | export interface PlatformStats { 2 | total: number; 3 | hasFollowed: number; 4 | notFollowed: number; 5 | } 6 | 7 | export interface UserCompleteStats { 8 | connections: { 9 | followers: number; 10 | following: number; 11 | totalEffectiveFollowers: number; 12 | }; 13 | matches: { 14 | bluesky: PlatformStats; 15 | mastodon: PlatformStats; 16 | }; 17 | updated_at: string; 18 | } 19 | 20 | export interface GlobalStats { 21 | users: { 22 | total: number; 23 | onboarded: number; 24 | }; 25 | connections: { 26 | followers: number; 27 | following: number; 28 | withHandle: number; 29 | withHandleBluesky: number; 30 | withHandleMastodon: number; 31 | followedOnBluesky: number; 32 | followedOnMastodon: number; 33 | }; 34 | updated_at: string; 35 | } 36 | 37 | export interface ReconnectionStats { 38 | connections: number; 39 | blueskyMappings: number; 40 | sources: number; 41 | } 42 | 43 | export interface RawStatsData { 44 | count: number; 45 | } 46 | 47 | export interface StatsError { 48 | error: string; 49 | status: number; 50 | } 51 | 52 | export interface StatsResponse { 53 | total_followers: number; 54 | total_following: number; 55 | total_sources: number; 56 | } 57 | 58 | export type UserStats = { 59 | following: number; 60 | followers: number; 61 | } -------------------------------------------------------------------------------- /src/lib/types/user.ts: -------------------------------------------------------------------------------- 1 | // Type de base pour un utilisateur 2 | export interface User { 3 | id: string; 4 | name?: string; 5 | twitter_id?: string; 6 | twitter_username?: string; 7 | twitter_image?: string; 8 | bluesky_id?: string; 9 | bluesky_username?: string; 10 | bluesky_image?: string; 11 | mastodon_id?: string; 12 | mastodon_username?: string; 13 | mastodon_image?: string; 14 | mastodon_instance?: string; 15 | email?: string; 16 | email_verified?: Date; 17 | image?: string; 18 | created_at: Date; 19 | updated_at: Date; 20 | has_onboarded: boolean; 21 | hqx_newsletter: boolean; 22 | oep_accepted: boolean; 23 | automatic_reconnect: boolean; 24 | research_accepted: boolean; 25 | have_seen_newsletter: boolean; 26 | personalized_support: boolean; 27 | } 28 | 29 | // Type pour les mises à jour utilisateur 30 | export type UserUpdate = Partial>; 31 | 32 | // Types spécifiques pour différents types de mises à jour 33 | export interface NewsletterUpdate extends Pick {} 34 | 35 | export interface ShareUpdate extends Pick {} 36 | 37 | // Type pour les événements de partage 38 | export interface ShareEvent { 39 | id?: string; 40 | source_id: string; 41 | platform: string; 42 | shared_at: string; 43 | success: boolean; 44 | created_at: string; 45 | } -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import createMiddleware from 'next-intl/middleware'; 2 | import { NextResponse } from 'next/server'; 3 | import type { NextRequest } from 'next/server'; 4 | 5 | const locales = ['fr', 'en', 'es', 'it', 'de', 'sv', 'pt']; 6 | const defaultLocale = 'en'; 7 | 8 | // Create the i18n middleware 9 | const intlMiddleware = createMiddleware({ 10 | locales, 11 | defaultLocale, 12 | localePrefix: 'always' 13 | }); 14 | 15 | // Middleware handler 16 | export default function middleware(request: NextRequest) { 17 | return intlMiddleware(request); 18 | } 19 | 20 | // Add matcher configuration to limit middleware execution 21 | export const config = { 22 | matcher: [ 23 | // Skip all internal paths (_next), API routes, and static files 24 | '/((?!api|_next/static|_next/image|favicon.ico).*)', 25 | ], 26 | }; -------------------------------------------------------------------------------- /src/services/api.ts: -------------------------------------------------------------------------------- 1 | export async function fetchUserStats() { 2 | const response = await fetch('/api/stats', { 3 | headers: { 'Cache-Control': 'no-cache' } 4 | }); 5 | return response.json(); 6 | } 7 | 8 | export async function fetchGlobalStats() { 9 | const response = await fetch('/api/stats/total', { 10 | headers: { 'Cache-Control': 'no-cache' } 11 | }); 12 | return response.json(); 13 | } 14 | 15 | export async function updateUserStats() { 16 | const response = await fetch('/api/update/user_stats', { 17 | method: 'POST', 18 | headers: { 'Content-Type': 'application/json' } 19 | }); 20 | return response.json(); 21 | } -------------------------------------------------------------------------------- /supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | .env 5 | -------------------------------------------------------------------------------- /supabase/migrations/001_create_schemas.sql: -------------------------------------------------------------------------------- 1 | create schema if not exists "next-auth"; 2 | create extension if not exists "uuid-ossp"; -------------------------------------------------------------------------------- /supabase/migrations/002_create_users_table.sql: -------------------------------------------------------------------------------- 1 | create table "next-auth"."users" ( 2 | "id" uuid not null default uuid_generate_v4(), 3 | "name" text, 4 | "twitter_id" text unique, 5 | "twitter_username" text unique, 6 | "twitter_image" text, 7 | "bluesky_id" text unique, 8 | "bluesky_username" text unique, 9 | "bluesky_image" text, 10 | "mastodon_id" text unique, 11 | "mastodon_username" text unique, 12 | "mastodon_image" text, 13 | "mastodon_instance" text, 14 | "email" text unique, 15 | "email_verified" timestamptz, 16 | "image" text, 17 | "created_at" timestamptz not null default now(), 18 | "updated_at" timestamptz not null default now(), 19 | "has_onboarded" boolean not null default false, 20 | "hqx_newsletter" boolean not null default false, 21 | "oep_accepted" boolean not null default false, 22 | "automatic_reconnect" boolean not null default false, 23 | "oep_accepted" boolean not null default false, 24 | "research_accepted" boolean not null default false, 25 | "have_seen_newsletter" boolean not null default false, 26 | primary key ("id"), 27 | unique("email") 28 | ); 29 | 30 | -- Create a trigger to automatically update the updated_at column 31 | create or replace function update_updated_at_column() 32 | returns trigger as $$ 33 | begin 34 | new.updated_at = now(); 35 | return new; 36 | end; 37 | $$ language 'plpgsql'; 38 | 39 | create trigger update_users_updated_at 40 | before update on "next-auth"."users" 41 | for each row 42 | execute procedure update_updated_at_column(); -------------------------------------------------------------------------------- /supabase/migrations/003_create_accounts_table.sql: -------------------------------------------------------------------------------- 1 | create table "next-auth"."accounts" ( 2 | "id" uuid not null default uuid_generate_v4(), 3 | "user_id" uuid not null, 4 | "type" text not null, 5 | "provider" text not null, 6 | "provider_account_id" text not null, 7 | "refresh_token" text, 8 | "access_token" text, 9 | "expires_at" bigint, 10 | "token_type" text, 11 | "scope" text, 12 | "id_token" text, 13 | "session_state" text, 14 | "created_at" timestamptz not null default now(), 15 | "updated_at" timestamptz not null default now(), 16 | primary key ("id"), 17 | foreign key ("user_id") references "next-auth"."users"("id") on delete cascade, 18 | unique("provider", "provider_account_id") 19 | ); 20 | 21 | create trigger update_accounts_updated_at 22 | before update on "next-auth"."accounts" 23 | for each row 24 | execute procedure update_updated_at_column(); -------------------------------------------------------------------------------- /supabase/migrations/004_create_sessions_table.sql: -------------------------------------------------------------------------------- 1 | create table "next-auth"."sessions" ( 2 | "id" uuid not null default uuid_generate_v4(), 3 | "user_id" uuid not null, 4 | "expires" timestamptz not null, 5 | "session_token" text not null, 6 | "created_at" timestamptz not null default now(), 7 | "updated_at" timestamptz not null default now(), 8 | primary key ("id"), 9 | foreign key ("user_id") references "next-auth"."users"("id") on delete cascade, 10 | unique("session_token") 11 | ); 12 | 13 | create trigger update_sessions_updated_at 14 | before update on "next-auth"."sessions" 15 | for each row 16 | execute procedure update_updated_at_column(); -------------------------------------------------------------------------------- /supabase/migrations/005_create_verification_tokens_table.sql: -------------------------------------------------------------------------------- 1 | create table "next-auth"."verification_tokens" ( 2 | "identifier" text not null, 3 | "token" text not null, 4 | "expires" timestamptz not null, 5 | "created_at" timestamptz not null default now(), 6 | primary key ("identifier", "token") 7 | ); -------------------------------------------------------------------------------- /supabase/migrations/006_create_user_data_table.sql: -------------------------------------------------------------------------------- 1 | -- Grant permissions to the service_role 2 | grant usage on schema "next-auth" to service_role; 3 | grant all privileges on all tables in schema "next-auth" to service_role; 4 | grant all privileges on all sequences in schema "next-auth" to service_role; 5 | 6 | -- Grant permissions to the authenticated role 7 | grant usage on schema "next-auth" to authenticated; 8 | grant all privileges on all tables in schema "next-auth" to authenticated; 9 | grant all privileges on all sequences in schema "next-auth" to authenticated; 10 | 11 | -- Grant permissions to the anon role 12 | grant usage on schema "next-auth" to anon; 13 | grant all privileges on all tables in schema "next-auth" to anon; 14 | 15 | -- Grant specific table permissions 16 | grant all privileges on table "next-auth".users to service_role, authenticated, anon; 17 | grant all privileges on table "next-auth".accounts to service_role, authenticated, anon; 18 | grant all privileges on table "next-auth".sessions to service_role, authenticated, anon; 19 | grant all privileges on table "next-auth".verification_tokens to service_role, authenticated, anon; 20 | 21 | create or replace function "next-auth".verify_twitter_token(token_param text) 22 | returns table ( 23 | user_id text, 24 | user_name text, 25 | user_email text, 26 | provider text, 27 | provider_account_id text 28 | ) 29 | security definer 30 | set search_path = public, "next-auth" 31 | as $$ 32 | begin 33 | return query 34 | select 35 | u.id::text as user_id, 36 | u.name as user_name, 37 | u.email as user_email, 38 | a.provider, 39 | a.provider_account_id 40 | from "next-auth".accounts a 41 | join "next-auth".users u on u.id = a.user_id::uuid 42 | where a.provider = 'twitter' 43 | and a.access_token = token_param 44 | and a.expires_at > extract(epoch from now())::bigint; 45 | end; 46 | $$ language plpgsql; 47 | 48 | 49 | 50 | -- Grant execute permission to authenticated users 51 | grant execute on function "next-auth".verify_twitter_token(text) to authenticated; 52 | grant execute on function "next-auth".verify_twitter_token(text) to anon; 53 | grant execute on function "next-auth".verify_twitter_token(text) to service_role; 54 | 55 | create table "public"."user_data" ( 56 | "id" uuid not null default uuid_generate_v4(), 57 | "user_id" uuid not null, 58 | "created_at" timestamptz not null default now(), 59 | "updated_at" timestamptz not null default now(), 60 | primary key ("id") 61 | ); -------------------------------------------------------------------------------- /supabase/migrations/007_create_row_level_security.sql: -------------------------------------------------------------------------------- 1 | -- Enable RLS on public.user_data 2 | alter table "public"."user_data" enable row level security; 3 | 4 | -- Create policy for user_data table 5 | create policy "Users can view and edit their own data" 6 | on "public"."user_data" 7 | for all 8 | using (auth.uid() = user_id) 9 | with check (auth.uid() = user_id); -------------------------------------------------------------------------------- /supabase/migrations/009_create_table_public_profiles.sql: -------------------------------------------------------------------------------- 1 | -- Create sources table (authenticated users) 2 | create table if not exists "public"."sources" ( 3 | "id" uuid references "next-auth"."users" on delete cascade primary key 4 | ); 5 | 6 | -- Enable RLS on sources 7 | alter table "public"."sources" enable row level security; 8 | 9 | -- Create RLS policies for sources 10 | create policy "Public sources are viewable by everyone" 11 | on sources for select using ( true ); 12 | 13 | create policy "Users can update their own source" 14 | on sources for update using ( auth.uid() = id ); 15 | 16 | -- Create targets table (Twitter accounts to follow) 17 | create table if not exists "public"."targets" ( 18 | "twitter_id" text primary key, 19 | "bluesky_handle" text, 20 | "bluesky_did" text 21 | ); 22 | 23 | -- Create sources_targets junction table 24 | create table if not exists "public"."sources_targets" ( 25 | "source_id" uuid references "public"."sources"(id) on delete cascade, 26 | "target_twitter_id" text references "public"."targets"(twitter_id) on delete cascade, 27 | "bluesky_handle" text, 28 | "has_follow_bluesky" boolean DEFAULT false, 29 | "followed_at_bluesky" timestamp with time zone, 30 | primary key (source_id, target_twitter_id) 31 | ); 32 | 33 | -- Enable RLS 34 | alter table "public"."targets" enable row level security; 35 | alter table "public"."sources_targets" enable row level security; 36 | 37 | -- RLS policies for targets 38 | create policy "Targets are viewable by everyone" 39 | on targets for select using ( true ); 40 | 41 | create policy "Authenticated users can create targets" 42 | on targets for insert with check ( auth.uid() in (select id from public.sources) ); 43 | 44 | -- RLS policies for sources_targets 45 | create policy "Anyone can view source-target relationships" 46 | on sources_targets for select using ( true ); 47 | 48 | create policy "Sources can create their own relationships" 49 | on sources_targets for insert 50 | with check ( auth.uid() = source_id ); 51 | 52 | create policy "Sources can delete their own relationships" 53 | on sources_targets for delete 54 | using ( auth.uid() = source_id ); 55 | 56 | create policy "Sources can update their own relationships" 57 | on sources_targets for update 58 | using ( auth.uid() = source_id ); -------------------------------------------------------------------------------- /supabase/migrations/010_create_bluesky_table.sql: -------------------------------------------------------------------------------- 1 | -- Create followers table to store Twitter followers 2 | create table if not exists "public"."followers" ( 3 | "twitter_id" text primary key, 4 | "bluesky_handle" text, 5 | "bluesky_did" text 6 | ); 7 | 8 | -- Enable RLS on followers 9 | alter table "public"."followers" enable row level security; 10 | 11 | -- Create RLS policies for followers 12 | create policy "Followers are viewable by everyone" 13 | on followers for select using ( true ); 14 | 15 | create policy "Authenticated users can create followers" 16 | on followers for insert 17 | with check ( auth.uid() in (select id from public.sources) ); 18 | 19 | -- Create sources_followers table to store relationships 20 | create table if not exists "public"."sources_followers" ( 21 | "source_id" uuid references public.sources(id) on delete cascade, 22 | "follower_id" text references public.followers(twitter_id) on delete cascade, 23 | "bluesky_handle" text, 24 | "has_follow_bluesky" boolean DEFAULT false, 25 | "followed_at_bluesky" timestamp with time zone, 26 | primary key (source_id, follower_id) 27 | ); 28 | 29 | -- Enable RLS on sources_followers 30 | alter table "public"."sources_followers" enable row level security; 31 | 32 | -- Create RLS policies for sources_followers 33 | create policy "Sources followers are viewable by everyone" 34 | on sources_followers for select using ( true ); 35 | 36 | create policy "Users can manage their own followers" 37 | on sources_followers for all 38 | using ( auth.uid() = source_id ); -------------------------------------------------------------------------------- /supabase/migrations/011_share_events.sql: -------------------------------------------------------------------------------- 1 | -- Drop existing table if it exists 2 | drop table if exists public.share_events; 3 | 4 | -- Create the share_events table 5 | create table if not exists public.share_events ( 6 | id uuid default gen_random_uuid() primary key, 7 | source_id uuid references public.sources(id) on delete cascade, 8 | platform text not null, 9 | shared_at timestamp with time zone default timezone('utc'::text, now()), 10 | success boolean default true, 11 | created_at timestamp with time zone default timezone('utc'::text, now()) 12 | ); 13 | 14 | -- Enable RLS 15 | alter table public.share_events enable row level security; 16 | 17 | -- Create RLS policies 18 | create policy "Users can view their own share events" 19 | on share_events for select 20 | using (auth.uid() = source_id); 21 | 22 | create policy "Users can create their own share events" 23 | on share_events for insert 24 | with check (auth.uid() = source_id); 25 | 26 | -- Create index for better query performance 27 | create index if not exists idx_share_events_source_id 28 | on public.share_events(source_id); 29 | 30 | -- Grant access to authenticated users 31 | grant insert, select on public.share_events to authenticated; -------------------------------------------------------------------------------- /supabase/migrations/012_create_import_jobs.sql: -------------------------------------------------------------------------------- 1 | -- Create import_jobs table 2 | create table if not exists public.import_jobs ( 3 | id uuid default uuid_generate_v4() primary key, 4 | user_id uuid references "next-auth"."users"(id), 5 | status text check (status in ('pending', 'processing', 'completed', 'failed')), 6 | total_items integer default 0, 7 | error_log text, 8 | job_type text check (job_type in ('large_file_import', 'direct_import')), 9 | file_paths text[], 10 | stats jsonb default '{"followers": 0, "following": 0, "total": 0, "processed": 0}'::jsonb, 11 | created_at timestamp with time zone default now(), 12 | updated_at timestamp with time zone default now(), 13 | started_at timestamp with time zone, 14 | completed_at timestamp with time zone 15 | ); 16 | 17 | -- Add indexes 18 | create index if not exists import_jobs_status_idx on import_jobs(status); 19 | create index if not exists import_jobs_user_id_idx on import_jobs(user_id); 20 | create index if not exists import_jobs_created_at_idx on import_jobs(created_at); 21 | 22 | -- Create the function to claim the next pending job 23 | CREATE OR REPLACE FUNCTION claim_next_pending_job(worker_id_input TEXT) 24 | RETURNS SETOF import_jobs AS $$ 25 | BEGIN 26 | RETURN QUERY 27 | WITH next_job AS ( 28 | UPDATE import_jobs 29 | SET 30 | status = 'processing', 31 | started_at = NOW(), 32 | updated_at = NOW() 33 | WHERE id = ( 34 | SELECT id 35 | FROM import_jobs 36 | WHERE status = 'pending' 37 | ORDER BY created_at ASC 38 | LIMIT 1 39 | FOR UPDATE SKIP LOCKED 40 | ) 41 | RETURNING * 42 | ) 43 | SELECT * FROM next_job; 44 | END; 45 | $$ LANGUAGE plpgsql; 46 | 47 | -- Enable RLS but allow service role full access 48 | alter table import_jobs enable row level security; 49 | 50 | create policy "Service role can manage import jobs" 51 | on import_jobs for all 52 | using (true) 53 | with check (true); 54 | 55 | -- Add policy for users to view their own jobs 56 | create policy "Users can view their own import jobs" 57 | on import_jobs for select 58 | using (auth.uid() = user_id); 59 | 60 | CREATE POLICY "Users can insert their own import jobs" 61 | ON import_jobs FOR INSERT 62 | TO authenticated 63 | WITH CHECK (auth.uid() = user_id); 64 | 65 | CREATE POLICY "Users can update their own import jobs" 66 | ON import_jobs FOR UPDATE 67 | TO authenticated 68 | USING (auth.uid() = user_id); -------------------------------------------------------------------------------- /supabase/migrations/013_create_mastodon_tables.sql: -------------------------------------------------------------------------------- 1 | create table "public"."mastodon_instances" ( 2 | "id" uuid not null default uuid_generate_v4(), 3 | "instance" text not null, 4 | "client_id" text not null, 5 | "client_secret" text not null, 6 | "created_at" timestamptz not null default now(), 7 | "updated_at" timestamptz not null default now(), 8 | primary key ("id") 9 | ); 10 | 11 | create trigger update_mastodon_instances_updated_at 12 | before update on "public"."mastodon_instances" 13 | for each row 14 | execute procedure update_updated_at_column(); 15 | -------------------------------------------------------------------------------- /supabase/migrations/014_create_bluesky_mapping.sql: -------------------------------------------------------------------------------- 1 | -- Create bluesky_mappings table to store Twitter to Bluesky handle mappings 2 | create table if not exists "public"."bluesky_mappings" ( 3 | "twitter_id" text primary key, 4 | "bluesky_handle" text not null, 5 | "imported_at" timestamp with time zone default timezone('utc'::text, now()) not null 6 | ); 7 | 8 | -- Enable RLS on bluesky_mappings 9 | alter table "public"."bluesky_mappings" enable row level security; 10 | 11 | -- Create RLS policies for bluesky_mappings 12 | create policy "Bluesky mappings are viewable by everyone" 13 | on bluesky_mappings for select using ( true ); 14 | 15 | create policy "Authenticated users can create bluesky mappings" 16 | on bluesky_mappings for insert 17 | with check ( auth.uid() in (select id from public.sources) ); 18 | 19 | -- Add indexes for better query performance 20 | create index if not exists idx_bluesky_mappings_twitter_id 21 | on public.bluesky_mappings(twitter_id); 22 | create index if not exists idx_bluesky_mappings_bluesky_handle 23 | on public.bluesky_mappings(bluesky_handle); -------------------------------------------------------------------------------- /supabase/migrations/017_update_sources.sql: -------------------------------------------------------------------------------- 1 | -- Add columns to sources_targets 2 | ALTER TABLE sources_targets 3 | ADD COLUMN IF NOT EXISTS bluesky_handle text, 4 | ADD COLUMN IF NOT EXISTS has_follow_bluesky boolean DEFAULT false, 5 | ADD COLUMN IF NOT EXISTS followed_at_bluesky timestamp with time zone; 6 | 7 | -- Create function for updating sources_targets 8 | CREATE OR REPLACE FUNCTION update_sources_targets_handle() RETURNS TRIGGER AS $$ 9 | BEGIN 10 | -- Update sources_targets when a new mapping is inserted or updated 11 | UPDATE sources_targets 12 | SET bluesky_handle = NEW.bluesky_handle 13 | WHERE target_twitter_id = NEW.twitter_id 14 | AND (bluesky_handle IS NULL OR bluesky_handle != NEW.bluesky_handle); 15 | 16 | RETURN NEW; 17 | END; 18 | $$ LANGUAGE plpgsql; 19 | 20 | -- Create or replace trigger 21 | DROP TRIGGER IF EXISTS trigger_update_sources_targets ON bluesky_mappings; 22 | CREATE TRIGGER trigger_update_sources_targets 23 | AFTER INSERT OR UPDATE OF bluesky_handle 24 | ON bluesky_mappings 25 | FOR EACH ROW 26 | EXECUTE FUNCTION update_sources_targets_handle(); 27 | 28 | -- Initial sync of existing data 29 | WITH to_update AS ( 30 | SELECT DISTINCT ON (st.target_twitter_id) 31 | st.target_twitter_id, 32 | bm.bluesky_handle 33 | FROM sources_targets st 34 | JOIN bluesky_mappings bm ON st.target_twitter_id = bm.twitter_id 35 | WHERE st.bluesky_handle IS NULL OR st.bluesky_handle != bm.bluesky_handle 36 | ) 37 | UPDATE sources_targets st 38 | SET bluesky_handle = tu.bluesky_handle 39 | FROM to_update tu 40 | WHERE st.target_twitter_id = tu.target_twitter_id; -------------------------------------------------------------------------------- /supabase/migrations/018_create_view_migration_bluesky.sql: -------------------------------------------------------------------------------- 1 | -- Créer la vue qui combine followers et following avec leurs handles Bluesky 2 | CREATE OR REPLACE VIEW migration_bluesky_view AS 3 | SELECT 4 | st.source_id as user_id, 5 | st.target_twitter_id as twitter_id, 6 | COALESCE(bm.bluesky_handle, st.bluesky_handle) as bluesky_handle, 7 | st.has_follow_bluesky, 8 | 'following' as relationship_type 9 | FROM sources_targets st 10 | LEFT JOIN bluesky_mappings bm ON st.target_twitter_id = bm.twitter_id; 11 | 12 | -- Créer un index sur user_id pour améliorer les performances 13 | CREATE INDEX IF NOT EXISTS idx_migration_bluesky_view_user_id_targets 14 | ON sources_targets(source_id); 15 | 16 | -- Ajouter des commentaires sur la vue 17 | COMMENT ON VIEW migration_bluesky_view IS 'Vue des following avec leurs handles Bluesky pour la migration'; 18 | COMMENT ON COLUMN migration_bluesky_view.user_id IS 'ID de l''utilisateur'; 19 | COMMENT ON COLUMN migration_bluesky_view.twitter_id IS 'ID Twitter du following'; 20 | COMMENT ON COLUMN migration_bluesky_view.bluesky_handle IS 'Handle Bluesky s''il existe'; 21 | COMMENT ON COLUMN migration_bluesky_view.has_follow_bluesky IS 'Si l''utilisateur suit déjà ce compte sur Bluesky'; 22 | COMMENT ON COLUMN migration_bluesky_view.relationship_type IS 'Type de relation (following)'; -------------------------------------------------------------------------------- /supabase/migrations/020_update_targets_with_mastodon.sql: -------------------------------------------------------------------------------- 1 | 2 | -- Migration pour ajouter le support Mastodon dans la table sources_targets 3 | 4 | -- Ajouter les nouvelles colonnes pour Mastodon 5 | ALTER TABLE "public"."sources_targets" 6 | ADD COLUMN IF NOT EXISTS "mastodon_id" text, 7 | ADD COLUMN IF NOT EXISTS "mastodon_username" text, 8 | ADD COLUMN IF NOT EXISTS "mastodon_instance" text, 9 | ADD COLUMN IF NOT EXISTS "has_follow_mastodon" boolean DEFAULT false, 10 | ADD COLUMN IF NOT EXISTS "followed_at_mastodon" timestamp with time zone; 11 | 12 | -- Créer des index pour optimiser les recherches 13 | CREATE INDEX IF NOT EXISTS idx_sources_targets_mastodon_id 14 | ON "public"."sources_targets"(mastodon_id); 15 | 16 | CREATE INDEX IF NOT EXISTS idx_sources_targets_mastodon_username 17 | ON "public"."sources_targets"(mastodon_username); 18 | 19 | CREATE INDEX IF NOT EXISTS idx_sources_targets_mastodon_instance 20 | ON "public"."sources_targets"(mastodon_instance); 21 | 22 | -- Mettre à jour les données Mastodon depuis twitter_mastodon_users 23 | UPDATE "public"."sources_targets" st 24 | SET 25 | mastodon_id = tmu.mastodon_id, 26 | mastodon_username = tmu.mastodon_username, 27 | mastodon_instance = tmu.mastodon_instance 28 | FROM "public"."twitter_mastodon_users" tmu 29 | WHERE st.target_twitter_id = tmu.twitter_id 30 | AND (st.mastodon_id IS NULL OR st.mastodon_username IS NULL OR st.mastodon_instance IS NULL); -------------------------------------------------------------------------------- /supabase/migrations/021_create_reconnect_queue.sql: -------------------------------------------------------------------------------- 1 | create table reconnect_queue ( 2 | id bigint generated by default as identity primary key, 3 | user_id uuid references next-auth.users not null, 4 | accounts jsonb not null, 5 | created_at timestamp with time zone default timezone('utc'::text, now()) not null, 6 | updated_at timestamp with time zone default timezone('utc'::text, now()) not null, 7 | status text default 'pending' not null 8 | ); 9 | 10 | create index idx_reconnect_queue_user_id on reconnect_queue(user_id); -------------------------------------------------------------------------------- /supabase/migrations/023_create_functions_for_stats.sql: -------------------------------------------------------------------------------- 1 | -- Fonction pour compter les followers 2 | CREATE OR REPLACE FUNCTION count_followers() 3 | RETURNS TABLE (count bigint) 4 | LANGUAGE sql 5 | STABLE -- Indique que la fonction ne modifie pas la BD 6 | PARALLEL SAFE -- Peut s'exécuter en parallèle 7 | AS $$ 8 | SELECT reltuples::bigint as count 9 | FROM pg_class 10 | WHERE relname = 'sources_followers'; 11 | $$; 12 | 13 | -- Fonction pour compter les targets (following) 14 | CREATE OR REPLACE FUNCTION count_targets() 15 | RETURNS TABLE (count bigint) 16 | LANGUAGE sql 17 | STABLE 18 | PARALLEL SAFE 19 | AS $$ 20 | SELECT reltuples::bigint as count 21 | FROM pg_class 22 | WHERE relname = 'sources_targets'; 23 | $$; 24 | 25 | -- Fonction pour compter les targets avec handle 26 | CREATE OR REPLACE FUNCTION count_targets_with_handle() 27 | RETURNS TABLE (count bigint) 28 | LANGUAGE sql 29 | STABLE 30 | PARALLEL SAFE 31 | AS $$ 32 | SELECT ( 33 | reltuples::bigint * ( 34 | COALESCE( 35 | (SELECT n_distinct 36 | FROM pg_stats 37 | WHERE tablename = 'sources_targets' 38 | AND attname IN ('bluesky_handle', 'mastodon_id') 39 | AND n_distinct > 0 40 | LIMIT 1 41 | ), 0.1) 42 | ) 43 | )::bigint as count 44 | FROM pg_class 45 | WHERE relname = 'sources_targets'; 46 | $$; 47 | 48 | -- Accorder les permissions d'exécution 49 | GRANT EXECUTE ON FUNCTION count_followers() TO authenticated, service_role; 50 | GRANT EXECUTE ON FUNCTION count_targets() TO authenticated, service_role; 51 | GRANT EXECUTE ON FUNCTION count_targets_with_handle() TO authenticated, service_role; 52 | 53 | -- Ajouter un ANALYZE pour mettre à jour les statistiques 54 | ANALYZE sources_followers; 55 | ANALYZE sources_targets; -------------------------------------------------------------------------------- /supabase/migrations_backup/001_create_schemas.sql: -------------------------------------------------------------------------------- 1 | create schema if not exists "next-auth"; 2 | create extension if not exists "uuid-ossp"; -------------------------------------------------------------------------------- /supabase/migrations_backup/20241217191141_create_schemas.sql: -------------------------------------------------------------------------------- 1 | create schema if not exists "next-auth"; 2 | create extension if not exists "uuid-ossp"; -------------------------------------------------------------------------------- /supabase/migrations_backup/20241217191212_create_users_table.sql: -------------------------------------------------------------------------------- 1 | create table if not exists "next-auth"."users" ( 2 | "id" uuid not null default uuid_generate_v4(), 3 | "name" text, 4 | "twitter_id" text unique, 5 | "twitter_username" text unique, 6 | "twitter_image" text unique, 7 | "bluesky_id" text unique, 8 | "bluesky_username" text unique, 9 | "bluesky_image" text unique, 10 | "mastodon_id" text unique, 11 | "mastodon_username" text unique, 12 | "mastodon_image" text unique, 13 | "mastodon_instance" text, 14 | "email" text unique, 15 | "email_verified" timestamptz, 16 | "image" text, 17 | "created_at" timestamptz not null default now(), 18 | "updated_at" timestamptz not null default now(), 19 | "has_onboarded" boolean not null default false, 20 | primary key ("id"), 21 | unique("email") 22 | ); 23 | 24 | -- Create a trigger to automatically update the updated_at column 25 | create or replace function update_updated_at_column() 26 | returns trigger as $$ 27 | begin 28 | new.updated_at = now(); 29 | return new; 30 | end; 31 | $$ language 'plpgsql'; 32 | 33 | create trigger update_users_updated_at 34 | before update on "next-auth"."users" 35 | for each row 36 | execute procedure update_updated_at_column(); -------------------------------------------------------------------------------- /supabase/migrations_backup/20241217191213_create_accounts_table.sql: -------------------------------------------------------------------------------- 1 | create table "next-auth"."accounts" ( 2 | "id" uuid not null default uuid_generate_v4(), 3 | "user_id" uuid not null, 4 | "type" text not null, 5 | "provider" text not null, 6 | "provider_account_id" text not null, 7 | "refresh_token" text, 8 | "access_token" text, 9 | "expires_at" bigint, 10 | "token_type" text, 11 | "scope" text, 12 | "id_token" text, 13 | "session_state" text, 14 | "created_at" timestamptz not null default now(), 15 | "updated_at" timestamptz not null default now(), 16 | primary key ("id"), 17 | foreign key ("user_id") references "next-auth"."users"("id") on delete cascade, 18 | unique("provider", "provider_account_id") 19 | ); 20 | 21 | create trigger update_accounts_updated_at 22 | before update on "next-auth"."accounts" 23 | for each row 24 | execute procedure update_updated_at_column(); -------------------------------------------------------------------------------- /supabase/migrations_backup/20241217191214_create_sessions_table.sql: -------------------------------------------------------------------------------- 1 | create table "next-auth"."sessions" ( 2 | "id" uuid not null default uuid_generate_v4(), 3 | "user_id" uuid not null, 4 | "expires" timestamptz not null, 5 | "session_token" text not null, 6 | "created_at" timestamptz not null default now(), 7 | "updated_at" timestamptz not null default now(), 8 | primary key ("id"), 9 | foreign key ("user_id") references "next-auth"."users"("id") on delete cascade, 10 | unique("session_token") 11 | ); 12 | 13 | create trigger update_sessions_updated_at 14 | before update on "next-auth"."sessions" 15 | for each row 16 | execute procedure update_updated_at_column(); -------------------------------------------------------------------------------- /supabase/migrations_backup/20241217191215_create_verification_tokens_table.sql: -------------------------------------------------------------------------------- 1 | create table "next-auth"."verification_tokens" ( 2 | "identifier" text not null, 3 | "token" text not null, 4 | "expires" timestamptz not null, 5 | "created_at" timestamptz not null default now(), 6 | primary key ("identifier", "token") 7 | ); -------------------------------------------------------------------------------- /supabase/migrations_backup/20241217191216_create_user_data_table.sql: -------------------------------------------------------------------------------- 1 | -- Grant permissions to the service_role 2 | grant usage on schema "next-auth" to service_role; 3 | grant all privileges on all tables in schema "next-auth" to service_role; 4 | grant all privileges on all sequences in schema "next-auth" to service_role; 5 | 6 | -- Grant permissions to the authenticated role 7 | grant usage on schema "next-auth" to authenticated; 8 | grant all privileges on all tables in schema "next-auth" to authenticated; 9 | grant all privileges on all sequences in schema "next-auth" to authenticated; 10 | 11 | -- Grant permissions to the anon role 12 | grant usage on schema "next-auth" to anon; 13 | grant all privileges on all tables in schema "next-auth" to anon; 14 | 15 | -- Grant specific table permissions 16 | grant all privileges on table "next-auth".users to service_role, authenticated, anon; 17 | grant all privileges on table "next-auth".accounts to service_role, authenticated, anon; 18 | grant all privileges on table "next-auth".sessions to service_role, authenticated, anon; 19 | grant all privileges on table "next-auth".verification_tokens to service_role, authenticated, anon; 20 | 21 | create or replace function "next-auth".verify_twitter_token(token_param text) 22 | returns table ( 23 | user_id text, 24 | user_name text, 25 | user_email text, 26 | provider text, 27 | provider_account_id text 28 | ) 29 | security definer 30 | set search_path = public, "next-auth" 31 | as $$ 32 | begin 33 | return query 34 | select 35 | u.id::text as user_id, 36 | u.name as user_name, 37 | u.email as user_email, 38 | a.provider, 39 | a.provider_account_id 40 | from "next-auth".accounts a 41 | join "next-auth".users u on u.id = a.user_id::uuid 42 | where a.provider = 'twitter' 43 | and a.access_token = token_param 44 | and a.expires_at > extract(epoch from now())::bigint; 45 | end; 46 | $$ language plpgsql; 47 | 48 | 49 | 50 | -- Grant execute permission to authenticated users 51 | grant execute on function "next-auth".verify_twitter_token(text) to authenticated; 52 | grant execute on function "next-auth".verify_twitter_token(text) to anon; 53 | grant execute on function "next-auth".verify_twitter_token(text) to service_role; 54 | 55 | create table "public"."user_data" ( 56 | "id" uuid not null default uuid_generate_v4(), 57 | "user_id" uuid not null, 58 | "created_at" timestamptz not null default now(), 59 | "updated_at" timestamptz not null default now(), 60 | primary key ("id") 61 | ); -------------------------------------------------------------------------------- /supabase/migrations_backup/20241217191217_create_row_level_security.sql: -------------------------------------------------------------------------------- 1 | -- Enable RLS on public.user_data 2 | alter table "public"."user_data" enable row level security; 3 | 4 | -- Create policy for user_data table 5 | create policy "Users can view and edit their own data" 6 | on "public"."user_data" 7 | for all 8 | using (auth.uid() = user_id) 9 | with check (auth.uid() = user_id); -------------------------------------------------------------------------------- /supabase/migrations_backup/20241217191221_share_events.sql: -------------------------------------------------------------------------------- 1 | -- Drop existing table if it exists 2 | drop table if exists public.share_events; 3 | 4 | -- Create the share_events table 5 | create table if not exists public.share_events ( 6 | id uuid default gen_random_uuid() primary key, 7 | source_id uuid references public.sources(id) on delete cascade, 8 | platform text not null, 9 | shared_at timestamp with time zone default timezone('utc'::text, now()), 10 | success boolean default true, 11 | created_at timestamp with time zone default timezone('utc'::text, now()) 12 | ); 13 | 14 | -- Enable RLS 15 | alter table public.share_events enable row level security; 16 | 17 | -- Create RLS policies 18 | create policy "Users can view their own share events" 19 | on share_events for select 20 | using (auth.uid() = source_id); 21 | 22 | create policy "Users can create their own share events" 23 | on share_events for insert 24 | with check (auth.uid() = source_id); 25 | 26 | -- Create index for better query performance 27 | create index if not exists idx_share_events_source_id 28 | on public.share_events(source_id); 29 | 30 | -- Grant access to authenticated users 31 | grant insert, select on public.share_events to authenticated; -------------------------------------------------------------------------------- /supabase/seed.sql: -------------------------------------------------------------------------------- 1 | insert into "next-auth"."users" ( 2 | id, 3 | name, 4 | email, 5 | email_verified, 6 | image, 7 | has_onboarded, 8 | twitter_id 9 | ) values ( 10 | uuid_generate_v4(), 11 | 'Test User', 12 | 'test@example.com', 13 | now(), 14 | null, 15 | false, 16 | '1234567890' 17 | ); 18 | 19 | insert into "public"."user_data" (user_id) 20 | select 21 | id as user_id 22 | from "next-auth"."users" 23 | where email = 'test@example.com'; -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | export default { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | colors: { 12 | background: "var(--background)", 13 | foreground: "var(--foreground)", 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | } satisfies Config; 19 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | export default { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | colors: { 12 | background: "var(--background)", 13 | foreground: "var(--foreground)", 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | } satisfies Config; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "next.config.js", "tailwind.config.js"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /watcher/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | 3 | # Créer un utilisateur non-root 4 | RUN addgroup -g 1001 appgroup && \ 5 | adduser -u 1001 -G appgroup -s /bin/sh -D appuser 6 | 7 | # Définir le répertoire de travail 8 | WORKDIR /app 9 | 10 | RUN npm install -g npm@11.2.0 11 | 12 | # Installer les dépendances système nécessaires (en tant que root) 13 | RUN apk add --no-cache python3 make g++ gcc 14 | 15 | # Copier les fichiers de dépendances avec les bonnes permissions 16 | COPY --chown=appuser:appgroup package*.json ./ 17 | 18 | # Donner les permissions à l'utilisateur non-root 19 | RUN chown -R appuser:appgroup /app 20 | 21 | # Passer à l'utilisateur non-root pour les installations npm 22 | USER appuser 23 | 24 | 25 | # Installer les dépendances npm 26 | RUN npm install 27 | 28 | # Copier le reste des fichiers avec les bonnes permissions 29 | COPY --chown=appuser:appgroup . . 30 | 31 | # Build de l'application 32 | RUN npm run build 33 | 34 | # Exposer le port si nécessaire 35 | EXPOSE 3000 36 | 37 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /watcher/monitor.log: -------------------------------------------------------------------------------- 1 | {"level":"info","message":"Rapport envoyé"} 2 | {"level":"info","message":"Rapport envoyé"} 3 | {"level":"info","message":"Monitoring started"} 4 | {"level":"info","message":"Rapport envoyé"} 5 | {"level":"info","message":"Rapport envoyé"} 6 | {"level":"info","message":"Monitoring started"} 7 | {"level":"info","message":"Rapport envoyé"} 8 | {"level":"info","message":"Rapport envoyé"} 9 | {"level":"info","message":"Monitoring started"} 10 | {"level":"info","message":"Rapport envoyé"} 11 | {"level":"info","message":"Rapport envoyé"} 12 | {"level":"info","message":"Monitoring started"} 13 | {"level":"info","message":"Rapport envoyé"} 14 | {"level":"info","message":"Rapport envoyé"} 15 | {"level":"info","message":"Monitoring started"} 16 | {"level":"info","message":"Rapport envoyé"} 17 | {"level":"info","message":"Rapport envoyé"} 18 | {"level":"info","message":"Monitoring started"} 19 | {"level":"info","message":"Rapport envoyé"} 20 | {"level":"info","message":"Rapport envoyé"} 21 | {"level":"info","message":"Monitoring started"} 22 | -------------------------------------------------------------------------------- /watcher/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openportability-watcher", 3 | "version": "1.0.0", 4 | "main": "dist/monitor.js", 5 | "scripts": { 6 | "build": "tsc", 7 | "start": "node dist/monitor.js", 8 | "dev": "ts-node src/monitor.ts" 9 | }, 10 | "dependencies": { 11 | "@supabase/supabase-js": "^2.39.0", 12 | "node-telegram-bot-api": "^0.64.0", 13 | "dotenv": "^16.3.1", 14 | "winston": "^3.11.0" 15 | }, 16 | "devDependencies": { 17 | "@types/node-telegram-bot-api": "^0.64.0", 18 | "@types/node": "^20.10.5", 19 | "typescript": "^5.3.3", 20 | "ts-node": "^10.9.2" 21 | } 22 | } -------------------------------------------------------------------------------- /watcher/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "outDir": "./dist", 6 | "rootDir": "./src", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true 11 | }, 12 | "include": ["src/**/*"], 13 | "exclude": ["node_modules"] 14 | } 15 | -------------------------------------------------------------------------------- /worker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | 3 | # Créer un utilisateur non-root 4 | RUN addgroup -g 1001 appgroup && \ 5 | adduser -u 1001 -G appgroup -s /bin/sh -D appuser 6 | 7 | # Installation de bash 8 | RUN apk add --no-cache bash 9 | 10 | RUN npm install -g npm@11.2.0 11 | 12 | # Définir le répertoire de travail 13 | WORKDIR /app 14 | 15 | # Copier les fichiers avec les bonnes permissions 16 | COPY --chown=appuser:appgroup package*.json ./ 17 | COPY --chown=appuser:appgroup . . 18 | 19 | # Donner les permissions à l'utilisateur non-root 20 | RUN chown -R appuser:appgroup /app 21 | 22 | # Passer à l'utilisateur non-root pour l'installation et le build 23 | USER appuser 24 | 25 | 26 | RUN npm install 27 | RUN npm run build 28 | 29 | # Rendre le script executable 30 | RUN chmod +x start-workers.sh 31 | 32 | ENV NODE_ENV=production 33 | 34 | CMD ["/bin/bash", "./start-workers.sh"] -------------------------------------------------------------------------------- /worker/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | WORKDIR /app 3 | 4 | # Install bash 5 | RUN apk add --no-cache bash 6 | 7 | COPY package*.json ./ 8 | COPY . . 9 | 10 | RUN npm install 11 | RUN chmod +x start-workers.sh 12 | 13 | ENV NODE_ENV=development 14 | 15 | CMD ["/bin/bash", "./start-workers.sh"] -------------------------------------------------------------------------------- /worker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "import-worker", 3 | "version": "1.0.0", 4 | "main": "dist/index.js", 5 | "scripts": { 6 | "build": "tsc", 7 | "start": "npx ts-node src/index.ts", 8 | "start:workers": "./start-workers.sh", 9 | "dev": "npx ts-node src/index.ts" 10 | }, 11 | "dependencies": { 12 | "@supabase/supabase-js": "^2.49.4", 13 | "dotenv": "^16.5.0", 14 | "date-fns": "^2.30.0" 15 | 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^18.19.87", 19 | "ts-node": "^10.9.2", 20 | "typescript": "^4.9.5" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /worker/src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility function to sleep for a specified number of milliseconds 3 | * @param ms Number of milliseconds to sleep 4 | * @returns Promise that resolves after the specified time 5 | */ 6 | export function sleep(ms: number): Promise { 7 | return new Promise(resolve => setTimeout(resolve, ms)); 8 | } -------------------------------------------------------------------------------- /worker/start-workers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Arrays to store PIDs 4 | declare -a FOLLOWER_PIDS=() 5 | declare -a FOLLOWING_PIDS=() 6 | 7 | # Workers pour les followers 8 | export JOB_TYPES=followers 9 | 10 | for i in {1..10}; do 11 | export WORKER_ID=worker${i}_followers 12 | npx ts-node src/index.ts & 13 | FOLLOWER_PIDS+=($!) 14 | echo "Started follower worker $i with PID ${FOLLOWER_PIDS[-1]}" 15 | done 16 | 17 | # Workers pour les following 18 | export JOB_TYPES=following 19 | 20 | for i in {1..10}; do 21 | export WORKER_ID=worker${i}_following 22 | npx ts-node src/index.ts & 23 | FOLLOWING_PIDS+=($!) 24 | echo "Started following worker $i with PID ${FOLLOWING_PIDS[-1]}" 25 | done 26 | 27 | # Attendre que tous les workers se terminent 28 | echo "Waiting for follower workers..." 29 | for pid in "${FOLLOWER_PIDS[@]}"; do 30 | wait $pid 31 | done 32 | 33 | echo "Waiting for following workers..." 34 | for pid in "${FOLLOWING_PIDS[@]}"; do 35 | wait $pid 36 | done 37 | 38 | echo "All workers completed" -------------------------------------------------------------------------------- /worker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "outDir": "./dist", 6 | "rootDir": "./src", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true 11 | }, 12 | "include": ["src/**/*"] 13 | } -------------------------------------------------------------------------------- /worker_refreshtoken/Dockerfile: -------------------------------------------------------------------------------- 1 | # Étape de build 2 | FROM node:18-alpine AS builder 3 | 4 | WORKDIR /app 5 | 6 | # Copier les fichiers de dépendances 7 | COPY package*.json ./ 8 | 9 | # Installer les dépendances 10 | RUN npm install 11 | 12 | # Copier le reste des fichiers 13 | COPY . . 14 | 15 | # Compiler le TypeScript 16 | RUN npm run build 17 | 18 | # Étape de production 19 | FROM node:18-alpine 20 | 21 | WORKDIR /app 22 | 23 | # Copier uniquement les fichiers nécessaires depuis l'étape de build 24 | COPY --from=builder /app/package*.json ./ 25 | COPY --from=builder /app/dist ./dist 26 | 27 | # Installer uniquement les dépendances de production 28 | RUN npm install --only=production 29 | 30 | # Démarrer le worker 31 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /worker_refreshtoken/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "token-refresh-worker", 3 | "version": "1.0.0", 4 | "description": "Worker pour rafraîchir les tokens Bluesky", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "dev": "ts-node src/index.ts", 8 | "build": "tsc", 9 | "start": "node dist/index.js" 10 | }, 11 | "dependencies": { 12 | "@atproto/api": "^0.6.23", 13 | "@supabase/supabase-js": "^2.39.0", 14 | "dotenv": "^16.3.1" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^20.10.4", 18 | "ts-node": "^10.9.1", 19 | "typescript": "^5.3.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /worker_refreshtoken/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "outDir": "./dist", 6 | "rootDir": "./src", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true 11 | }, 12 | "include": ["src/**/*"], 13 | "exclude": ["node_modules"] 14 | } --------------------------------------------------------------------------------