Compare commits
No commits in common. "main" and "v2.8.3" have entirely different histories.
604 changed files with 30921 additions and 55734 deletions
|
|
@ -40,7 +40,6 @@ STORAGE_PROVIDER="local"
|
|||
#NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY=""
|
||||
|
||||
# Social Media API Settings
|
||||
X_URL=""
|
||||
X_API_KEY=""
|
||||
X_API_SECRET=""
|
||||
LINKEDIN_CLIENT_ID=""
|
||||
|
|
@ -77,9 +76,6 @@ MASTODON_URL="https://mastodon.social"
|
|||
MASTODON_CLIENT_ID=""
|
||||
MASTODON_CLIENT_SECRET=""
|
||||
|
||||
# Chrome Extension Settings (for cookie-based platform integrations like Skool)
|
||||
EXTENSION_ID=""
|
||||
|
||||
# Misc Settings
|
||||
OPENAI_API_KEY=""
|
||||
NEXT_PUBLIC_DISCORD_SUPPORT=""
|
||||
|
|
|
|||
19
.github/ISSUE_TEMPLATE/01_installation.prolem.yml
vendored
Normal file
19
.github/ISSUE_TEMPLATE/01_installation.prolem.yml
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
name: "🙏🏻 Installation Problem"
|
||||
description: "Report an issue with installation"
|
||||
title: "Installation Problem"
|
||||
labels: ["type: installation"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: For installation issues, please visit our [Discord Support](https://discord.postiz.com) for assistance.
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: For installation issues, please visit our https://discord.postiz.com for assistance.
|
||||
description: For installation issues, please visit our [Discord Support](https://discord.postiz.com) for assistance.
|
||||
placeholder: |
|
||||
For installation issues, please visit our https://discord.postiz.com for assistance.
|
||||
Please do not save this issue - do not submit installation issues on GitHub.
|
||||
|
||||
14
.github/ISSUE_TEMPLATE/config.yml
vendored
14
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1,14 +0,0 @@
|
|||
# Disable the default option to open a blank issue
|
||||
blank_issues_enabled: true
|
||||
|
||||
# Define your custom links
|
||||
contact_links:
|
||||
# The first link definition
|
||||
- name: 🙏 Installation Issue
|
||||
url: https://discord.postiz.com
|
||||
about: If you have an installation / configuration issue.
|
||||
|
||||
# You can add more links if needed
|
||||
- name: Security Issue
|
||||
url: https://github.com/gitroomhq/postiz-app/security/advisories/new
|
||||
about: Please submit security Issues our GitHub Security Advisories.
|
||||
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -1,5 +1,3 @@
|
|||
<!-- Remember to first apply via [the contribution form](https://contribute.postiz.com/p/postiz) before submitting a PR. -->
|
||||
|
||||
# What kind of change does this PR introduce?
|
||||
|
||||
eg: Bug fix, feature, docs update, ...
|
||||
|
|
@ -17,6 +15,5 @@ eg: Did you discuss this change with anybody before working on it (not required,
|
|||
Put a "X" in the boxes below to indicate you have followed the checklist;
|
||||
|
||||
- [ ] I have read the [CONTRIBUTING](https://github.com/gitroomhq/postiz-app/blob/main/CONTRIBUTING.md) guide.
|
||||
- [ ] I confirm I have not used AI to submit this PR or generate code for it.
|
||||
- [ ] I checked that there were no similar issues or PRs already open for this.
|
||||
- [ ] This PR fixes just ONE issue
|
||||
- [ ] I checked that there were not similar issues or PRs already open for this.
|
||||
- [ ] This PR fixes just ONE issue (do not include multiple issues or types of change in the same PR) For example, don't try and fix a UI issue and include new dependencies in the same PR.
|
||||
|
|
|
|||
BIN
.github/sponsors/hostinger.png
vendored
BIN
.github/sponsors/hostinger.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 69 KiB |
69
.github/workflows/build
vendored
Normal file
69
.github/workflows/build
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
---
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: ['20.17.0']
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
${{ env.STORE_PATH }}
|
||||
${{ github.workspace }}/.next/cache
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Get Commit SHA (short)
|
||||
id: get_version
|
||||
run: |
|
||||
# Get the short 8-character commit SHA
|
||||
VERSION=$(git rev-parse --short=8 HEAD)
|
||||
echo "Commit SHA is $VERSION"
|
||||
echo "tag=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: SonarQube Analysis (Branch)
|
||||
uses: SonarSource/sonarqube-scan-action@v6
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
||||
with:
|
||||
args: >
|
||||
-Dsonar.projectVersion=${{ steps.get_version.outputs.tag }}
|
||||
2
.github/workflows/build-containers.yml
vendored
2
.github/workflows/build-containers.yml
vendored
|
|
@ -53,8 +53,6 @@ jobs:
|
|||
-f Dockerfile.dev \
|
||||
-t ghcr.io/gitroomhq/postiz-app:${{ env.CONTAINERVER }}-${{ matrix.arch }} \
|
||||
--build-arg NEXT_PUBLIC_VERSION=${{ env.NEXT_PUBLIC_VERSION }} \
|
||||
--pull \
|
||||
--no-cache \
|
||||
--provenance=false --sbom=false \
|
||||
--output "type=registry,name=ghcr.io/gitroomhq/postiz-app:${{ env.CONTAINERVER }}-${{ matrix.arch }}" .
|
||||
|
||||
|
|
|
|||
71
.github/workflows/build-pr
vendored
Normal file
71
.github/workflows/build-pr
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
name: Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: ['20.17.0']
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
environment:
|
||||
name: build-pr
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
${{ env.STORE_PATH }}
|
||||
${{ github.workspace }}/.next/cache
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Get Commit SHA (short)
|
||||
id: get_version
|
||||
run: |
|
||||
# Get the short 8-character commit SHA
|
||||
VERSION=$(git rev-parse --short=8 HEAD)
|
||||
echo "Commit SHA is $VERSION"
|
||||
echo "tag=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: SonarQube Analysis (Pull Request)
|
||||
uses: SonarSource/sonarqube-scan-action@v6
|
||||
with:
|
||||
args: >
|
||||
-Dsonar.projectVersion=${{ steps.get_version.outputs.tag }}
|
||||
-Dsonar.pullrequest.key=${{ github.event.pull_request.number }}
|
||||
-Dsonar.pullrequest.branch=${{ github.event.pull_request.head.ref }}
|
||||
-Dsonar.pullrequest.base=${{ github.event.pull_request.base.ref }}
|
||||
54
.github/workflows/build.yml
vendored
54
.github/workflows/build.yml
vendored
|
|
@ -1,54 +0,0 @@
|
|||
---
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
merge_group:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: ['22.12.0']
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
# - name: Setup pnpm cache
|
||||
# uses: actions/cache@v4
|
||||
# with:
|
||||
# path: |
|
||||
# ${{ env.STORE_PATH }}
|
||||
# ${{ github.workspace }}/.next/cache
|
||||
# key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
|
|
@ -1,16 +1,14 @@
|
|||
---
|
||||
name: "Code Quality Analysis"
|
||||
name: "Code Quality Analysis"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev1
|
||||
paths:
|
||||
- apps/**
|
||||
- '!apps/docs/**'
|
||||
- libraries/**
|
||||
merge_group:
|
||||
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
|
|
|
|||
2
.github/workflows/eslint
vendored
2
.github/workflows/eslint
vendored
|
|
@ -39,7 +39,7 @@ jobs:
|
|||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
**/pnpm-lock.yaml
|
||||
**/package-lock.json
|
||||
|
||||
- name: Install ESLint
|
||||
run: |
|
||||
|
|
|
|||
38
.github/workflows/pr-docker-build.yml
vendored
Normal file
38
.github/workflows/pr-docker-build.yml
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
name: Build and Publish PR Docker Image
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions: write-all
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
environment:
|
||||
name: build-pr
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
|
||||
- name: Set image tag
|
||||
id: vars
|
||||
run: echo "IMAGE_TAG=ghcr.io/gitroomhq/postiz-app-pr:${{ github.event.pull_request.number }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build Docker image from Dockerfile.dev
|
||||
run: docker build -f Dockerfile.dev -t $IMAGE_TAG .
|
||||
|
||||
- name: Push Docker image to GHCR
|
||||
run: docker push $IMAGE_TAG
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -19,7 +19,7 @@ node_modules
|
|||
.vscode/*
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
|
|
@ -58,6 +58,3 @@ Thumbs.db
|
|||
.secrets/
|
||||
libraries/plugins/src/plugins.ts
|
||||
i18n.cache
|
||||
|
||||
# Generated by apps/frontend/scripts/fetch-gtm.mjs on install
|
||||
apps/frontend/public/g.js
|
||||
|
|
|
|||
61
CLAUDE.md
61
CLAUDE.md
|
|
@ -1,61 +0,0 @@
|
|||
This project is Postiz, a tool to schedule social media and chat posts to 28+ channels.
|
||||
You can add posts to the calendar, they will be added into a workflow and posted at the right time.
|
||||
You can find things like:
|
||||
- Schedule posts
|
||||
- Calendar view
|
||||
- Analytics
|
||||
- Team management
|
||||
- Media library
|
||||
|
||||
This project is a monorepo with a root only package.json of dependencies.
|
||||
Made with PNPM.
|
||||
We have 3 important folders
|
||||
|
||||
- apps/backend - this is where the API code is (NESTJS)
|
||||
- apps/orchestrator - this is temporal, it's for background jobs (NESTJS) it contains all the workflows and activities
|
||||
- apps/frontend - this is the code of the frontend (Vite ReactJS)
|
||||
- /libraries contains a lot of services shared between backend and orchestrator and frontend components.
|
||||
|
||||
We are using only pnpm, don't use any other dependency manager.
|
||||
Never install frontend components from npmjs, focus on writing native components.
|
||||
|
||||
The project uses tailwind 3, before writing any component look at:
|
||||
- /apps/frontend/src/app/colors.scss
|
||||
- /apps/frontend/src/app/global.scss
|
||||
- /apps/frontend/tailwind.config.js
|
||||
|
||||
All the --color-custom* are deprecated, don't use them.
|
||||
|
||||
And check other components in the system before to get the right design.
|
||||
|
||||
When working on the backend we need to pass the 3 layers:
|
||||
Controller >> Service >> Repository (no shortcuts)
|
||||
In some cases we will have
|
||||
Controller >> Mananger >> Service >> Repository.
|
||||
|
||||
Most of the server logic should be inside of libs/server.
|
||||
The backend repository is mostly used to write controller, and import files from libs.server.
|
||||
|
||||
For the frontend follow this:
|
||||
- Many of the UI components lives in /apps/frontend/src/components/ui
|
||||
- Routing is in /apps/frontend/src/app
|
||||
- Components are in /apps/frontend/src/components
|
||||
- always use SWR to fetch stuff, and use "useFetch" hook from /libraries/helpers/src/utils/custom.fetch.tsx
|
||||
|
||||
When using SWR, each one have to be in a seperate hook and must comply with react-hooks/rules-of-hooks, never put eslint-disable-next-line on it.
|
||||
|
||||
It means that this is valid:
|
||||
const useCommunity = () => {
|
||||
return useSWR....
|
||||
}
|
||||
|
||||
This is not valid:
|
||||
const useCommunity = () => {
|
||||
return {
|
||||
communities: () => useSWR<CommunitiesListResponse>("communities", getCommunities),
|
||||
providers: () => useSWR<ProvidersListResponse>("providers", getProviders),
|
||||
};
|
||||
}
|
||||
|
||||
- Linting of the project can run only from the root.
|
||||
- Use only pnpm.
|
||||
|
|
@ -6,10 +6,6 @@ Contributions are welcome - code, docs, whatever it might be! If this is your fi
|
|||
|
||||
The main documentation site has a [developer guide](https://docs.postiz.com/developer-guide) . That guide provides you a good understanding of the project structure, and how to setup your development environment. Read this document after you have read that guide. This document is intended to provide you a good understanding of how to submit your first contribution.
|
||||
|
||||
## Apply via the contribution form
|
||||
|
||||
To submit your contribution, please fill out the [contribution form](https://contribute.postiz.com/p/postiz). This helps us evaluate whether your contribution is a good fit for the project. We will review your submission and get back to you as soon as possible.
|
||||
|
||||
## Write code with others
|
||||
|
||||
This is an open source project, with an open and welcoming community that is always keen to welcome new contributors. We recommend the two best ways to interact with the community are:
|
||||
|
|
@ -31,11 +27,6 @@ Contributions can include:
|
|||
- **Feature requests:** Suggesting new capabilities or integrations.
|
||||
- **Bug reports:** Identifying and reporting issues.
|
||||
|
||||
## AI
|
||||
|
||||
To ensure the quality and maintainability of the codebase, **we do not accept Pull Requests generated primarily by AI tools** (e.g., ChatGPT, GitHub Copilot, Claude Code, etc.).
|
||||
All contributions must be the original work of the author. We reserve the right to close any PR that appears to be AI-generated without further review.
|
||||
|
||||
## How to contribute
|
||||
|
||||
This project follows a Fork/Feature Branch/Pull Request model. If you're not familiar with this, here's how it works:
|
||||
|
|
|
|||
|
|
@ -1,18 +1,11 @@
|
|||
FROM node:22.20-bookworm-slim
|
||||
FROM node:22.20-alpine
|
||||
ARG NEXT_PUBLIC_VERSION
|
||||
ENV NEXT_PUBLIC_VERSION=$NEXT_PUBLIC_VERSION
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
g++ \
|
||||
make \
|
||||
python3-pip \
|
||||
bash \
|
||||
nginx \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN addgroup --system www \
|
||||
&& adduser --system --ingroup www --home /www --shell /usr/sbin/nologin www \
|
||||
&& mkdir -p /www \
|
||||
&& chown -R www:www /www /var/lib/nginx
|
||||
RUN apk add --no-cache g++ make py3-pip bash nginx
|
||||
RUN adduser -D -g 'www' www
|
||||
RUN mkdir /www
|
||||
RUN chown -R www:www /var/lib/nginx
|
||||
RUN chown -R www:www /www
|
||||
|
||||
|
||||
RUN npm --no-update-notifier --no-fund --global install pnpm@10.6.1 pm2
|
||||
|
|
|
|||
30
README.md
30
README.md
|
|
@ -1,3 +1,9 @@
|
|||
<p align="center">
|
||||
<a href="https://github.com/growchief/growchief">
|
||||
<img alt="automate" src="https://github.com/user-attachments/assets/d760188d-8d56-4b05-a6c1-c57e67ef25cd" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://postiz.com/" target="_blank">
|
||||
<picture>
|
||||
|
|
@ -13,7 +19,6 @@
|
|||
</a>
|
||||
</p>
|
||||
|
||||
<h3 align="center"><strong><a href="https://github.com/gitroomhq/postiz-agent">NEW: check out Postiz agent CLI! perfect for OpenClaw and other agents</a></strong></h3>
|
||||
<div align="center">
|
||||
<strong>
|
||||
<h2>Your ultimate AI social media scheduling tool</h2><br />
|
||||
|
|
@ -65,14 +70,11 @@
|
|||
<a href="https://apps.make.com/postiz">Make.com integration</a>
|
||||
</p>
|
||||
|
||||
<br /><br />
|
||||
|
||||
## 🔌 See the leading Postiz features
|
||||
<br />
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=BdsCVvEYgHU" target="_blank">
|
||||
<img alt="Postiz" src="https://github.com/user-attachments/assets/8b9b7939-da1a-4be5-95be-42c6fce772de" />
|
||||
</a>
|
||||
<video src="https://github.com/user-attachments/assets/05436a01-19c8-4827-b57f-05a5e7637a67" width="100%" />
|
||||
</p>
|
||||
|
||||
## ✨ Features
|
||||
|
|
@ -81,15 +83,6 @@
|
|||
| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
|  |  |
|
||||
|
||||
### Our Sponsors
|
||||
|
||||
| Sponsor | Logo | Description |
|
||||
|---------|:-----------------------------------------------------------------------:|-----------------|
|
||||
| [Hostinger](https://www.hostinger.com/vps/docker/postiz?ref=postiz) | <img src=".github/sponsors/hostinger.png" alt="Hostinger" width="500"/> | Hostinger is on a mission to make online success possible for anyone – from developers to aspiring bloggers and business owners |
|
||||
| [Virlo](https://dev.virlo.ai/?ref=postiz) | <img src="https://github.com/user-attachments/assets/25182598-5344-45fc-b9cd-e4cfa16aabfd" alt="Virlo" width="500"/> | Virlo is the #1 social media trend spotting and all-in-one GTM tool for teams leveraging short-form video |
|
||||
|
||||
|
||||
|
||||
# Intro
|
||||
|
||||
- Schedule all your social media posts (many AI features)
|
||||
|
|
@ -97,15 +90,14 @@
|
|||
- Collaborate with other team members to exchange or buy posts.
|
||||
- Invite your team members to collaborate, comment, and schedule posts.
|
||||
- At the moment there is no difference between the hosted version to the self-hosted version
|
||||
- Perfect for automation (API) with platforms like N8N, Make.com, Zapier, etc.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- Pnpm workspaces (Monorepo)
|
||||
- NX (Monorepo)
|
||||
- NextJS (React)
|
||||
- NestJS
|
||||
- Prisma (Default to PostgreSQL)
|
||||
- Temporal
|
||||
- Redis (BullMQ)
|
||||
- Resend (email notifications)
|
||||
|
||||
## Quick Start
|
||||
|
|
@ -132,7 +124,7 @@ Link: https://opencollective.com/postiz
|
|||
|
||||
## Star History
|
||||
|
||||
[](https://www.star-history.com/#gitroomhq/postiz-app&type=date&legend=top-left)
|
||||
[](https://www.star-history.com/#gitroomhq/postiz-app&Date)
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
46
SECURITY.md
46
SECURITY.md
|
|
@ -4,48 +4,26 @@
|
|||
|
||||
The Postiz app is committed to ensuring the security and integrity of our users' data. This security policy outlines our procedures for handling security vulnerabilities and our disclosure policy.
|
||||
|
||||
## Scope
|
||||
|
||||
We, at Postiz (gitroomhq), cover the following scopes for vulnerability disclosures:
|
||||
|
||||
- The core repository for `postiz-app` (github.com/gitroomhq/postiz-app)
|
||||
- All `gitroomhq` repositories that are official components, tooling, or integrations of Postiz
|
||||
- Official Postiz container images published under `gitroomhq` on GHCR
|
||||
- Official Postiz CLI tools and NPM packages (NPM org: @postiz)
|
||||
- Postiz-Cloud related infrastructure & services. (API, Frontend, Configurations etc.)
|
||||
- Plugins for Postiz maintained within the `gitroomhq` organization
|
||||
|
||||
Vulnerabilities in third-party dependencies or user-hosted infrastructure are outside of this scope.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
This project currently only supports the latest release. We recommend that users always use the latest version of the Postiz app to ensure they have the latest security patches.
|
||||
*CVE IDs will only be assigned to vulnerabilities affecting currently supported versions.*
|
||||
|
||||
## Reporting Security Vulnerabilities
|
||||
|
||||
If you discover a security vulnerability in the Postiz app, please report it through the [GitHub Security Advisory system](https://github.com/gitroomhq/postiz-app/security/advisories/new).
|
||||
If you discover a security vulnerability in the Postiz app, please report it to us privately via email to one of the maintainers:
|
||||
|
||||
- @nevo-david
|
||||
- @ennogelhaus ([email](mailto:gelhausenno@outlook.de))
|
||||
|
||||
When reporting a security vulnerability, please provide as much detail as possible, including:
|
||||
|
||||
- A clear description of the vulnerability
|
||||
- Proof of concept (PoC), where possible
|
||||
- Steps to reproduce the vulnerability
|
||||
- Any relevant code or configuration files
|
||||
|
||||
If the report has immediate urgency, please contact one (or more) of the maintainers via email:
|
||||
## Supported Versions
|
||||
|
||||
- @egelhaus ([E-Mail](mailto:egelhaus@ennogelhaus.de))
|
||||
|
||||
### AI Reports
|
||||
|
||||
Reports that appear to be LLM-generated without meaningful human analysis — typically lacking a working proof of concept, reproducible steps, or accurate impact assessment — will be closed without detailed response.
|
||||
|
||||
Reports that include AI-assisted analysis are welcome provided they have been validated by the reporter and include a proof of concept, reproduction steps, and impact assessment.
|
||||
This project currently only supports the latest release. We recommend that users always use the latest version of the Postiz app to ensure they have the latest security patches.
|
||||
|
||||
## Disclosure Guidelines
|
||||
|
||||
We follow a private disclosure policy. If you discover a security vulnerability, please report it to us privately via GitHub Security Advisories, and if immediate urgency, via email as listed above. We will respond promptly to reports of vulnerabilities and work to resolve them as quickly as possible.
|
||||
We follow a private disclosure policy. If you discover a security vulnerability, please report it to us privately via email to one of the maintainers listed above. We will respond promptly to reports of vulnerabilities and work to resolve them as quickly as possible.
|
||||
|
||||
We will not publicly disclose security vulnerabilities until a patch or fix is available to prevent malicious actors from exploiting the vulnerability before a fix is released.
|
||||
|
||||
|
|
@ -58,12 +36,8 @@ We take security vulnerabilities seriously and will respond promptly to reports
|
|||
- Releasing the patch or fix as soon as possible.
|
||||
- Notifying users of the vulnerability and the patch or fix.
|
||||
|
||||
## Response Timelines
|
||||
## Template Attribution
|
||||
|
||||
We aim to follow these timelines:
|
||||
This SECURITY.md file is based on the [GitHub Security Policy Template](https://docs.github.com/en/code-security/getting-started/adding-a-security-policy-to-your-repository).
|
||||
|
||||
- **Initial Acknowledgment:** Within 72 hours of initial report.
|
||||
- **Completed Triage / Verification:** Within 7 days of initial acknowledgment.
|
||||
- **Critical Issue Remediation:** Within 90 days of completed triage.
|
||||
- **Non-Critical Issue Remediation:** Within 180 days of completed triage.
|
||||
- **CVE Publication:** Within 24 hours of remediation release.
|
||||
Thank you for helping to keep the `postiz-app` secure!
|
||||
|
|
|
|||
|
|
@ -16,10 +16,13 @@ import { MediaController } from '@gitroom/backend/api/routes/media.controller';
|
|||
import { UploadModule } from '@gitroom/nestjs-libraries/upload/upload.module';
|
||||
import { BillingController } from '@gitroom/backend/api/routes/billing.controller';
|
||||
import { NotificationsController } from '@gitroom/backend/api/routes/notifications.controller';
|
||||
import { MarketplaceController } from '@gitroom/backend/api/routes/marketplace.controller';
|
||||
import { MessagesController } from '@gitroom/backend/api/routes/messages.controller';
|
||||
import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
|
||||
import { ExtractContentService } from '@gitroom/nestjs-libraries/openai/extract.content.service';
|
||||
import { CodesService } from '@gitroom/nestjs-libraries/services/codes.service';
|
||||
import { CopilotController } from '@gitroom/backend/api/routes/copilot.controller';
|
||||
import { AgenciesController } from '@gitroom/backend/api/routes/agencies.controller';
|
||||
import { PublicController } from '@gitroom/backend/api/routes/public.controller';
|
||||
import { RootController } from '@gitroom/backend/api/routes/root.controller';
|
||||
import { TrackService } from '@gitroom/nestjs-libraries/track/track.service';
|
||||
|
|
@ -31,19 +34,6 @@ import { AutopostController } from '@gitroom/backend/api/routes/autopost.control
|
|||
import { SetsController } from '@gitroom/backend/api/routes/sets.controller';
|
||||
import { ThirdPartyController } from '@gitroom/backend/api/routes/third-party.controller';
|
||||
import { MonitorController } from '@gitroom/backend/api/routes/monitor.controller';
|
||||
import { NoAuthIntegrationsController } from '@gitroom/backend/api/routes/no.auth.integrations.controller';
|
||||
import { EnterpriseController } from '@gitroom/backend/api/routes/enterprise.controller';
|
||||
import { OAuthAppController } from '@gitroom/backend/api/routes/oauth-app.controller';
|
||||
import { ApprovedAppsController } from '@gitroom/backend/api/routes/approved-apps.controller';
|
||||
import { OAuthController, OAuthAuthorizedController } from '@gitroom/backend/api/routes/oauth.controller';
|
||||
import { AnnouncementsController } from '@gitroom/backend/api/routes/announcements.controller';
|
||||
import { AdminController } from '@gitroom/backend/api/routes/admin.controller';
|
||||
import { AuthProviderManager } from '@gitroom/backend/services/auth/providers/providers.manager';
|
||||
import { GithubProvider } from '@gitroom/backend/services/auth/providers/github.provider';
|
||||
import { GoogleProvider } from '@gitroom/backend/services/auth/providers/google.provider';
|
||||
import { FarcasterProvider } from '@gitroom/backend/services/auth/providers/farcaster.provider';
|
||||
import { WalletProvider } from '@gitroom/backend/services/auth/providers/wallet.provider';
|
||||
import { OauthProvider } from '@gitroom/backend/services/auth/providers/oauth.provider';
|
||||
|
||||
const authenticatedController = [
|
||||
UsersController,
|
||||
|
|
@ -54,17 +44,15 @@ const authenticatedController = [
|
|||
MediaController,
|
||||
BillingController,
|
||||
NotificationsController,
|
||||
MarketplaceController,
|
||||
MessagesController,
|
||||
CopilotController,
|
||||
AgenciesController,
|
||||
WebhookController,
|
||||
SignatureController,
|
||||
AutopostController,
|
||||
SetsController,
|
||||
ThirdPartyController,
|
||||
OAuthAppController,
|
||||
ApprovedAppsController,
|
||||
OAuthAuthorizedController,
|
||||
AnnouncementsController,
|
||||
AdminController,
|
||||
];
|
||||
@Module({
|
||||
imports: [UploadModule],
|
||||
|
|
@ -74,9 +62,6 @@ const authenticatedController = [
|
|||
AuthController,
|
||||
PublicController,
|
||||
MonitorController,
|
||||
EnterpriseController,
|
||||
NoAuthIntegrationsController,
|
||||
OAuthController,
|
||||
...authenticatedController,
|
||||
],
|
||||
providers: [
|
||||
|
|
@ -92,12 +77,6 @@ const authenticatedController = [
|
|||
TrackService,
|
||||
ShortLinkService,
|
||||
Nowpayments,
|
||||
AuthProviderManager,
|
||||
GithubProvider,
|
||||
GoogleProvider,
|
||||
FarcasterProvider,
|
||||
WalletProvider,
|
||||
OauthProvider,
|
||||
],
|
||||
get exports() {
|
||||
return [...this.imports, ...this.providers];
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
HttpException,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
|
||||
import { User } from '@prisma/client';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { ErrorsService } from '@gitroom/nestjs-libraries/database/prisma/errors/errors.service';
|
||||
|
||||
@ApiTags('Admin')
|
||||
@Controller('/admin')
|
||||
export class AdminController {
|
||||
constructor(private _errorsService: ErrorsService) {}
|
||||
|
||||
private assertSuperAdmin(user: User) {
|
||||
if (!user?.isSuperAdmin) {
|
||||
throw new HttpException('Unauthorized', 400);
|
||||
}
|
||||
}
|
||||
|
||||
@Get('/errors')
|
||||
async listErrors(
|
||||
@GetUserFromRequest() user: User,
|
||||
@Query('page') page?: string,
|
||||
@Query('limit') limit?: string,
|
||||
@Query('platform') platform?: string,
|
||||
@Query('email') email?: string,
|
||||
@Query('unknownFirst') unknownFirst?: string
|
||||
) {
|
||||
this.assertSuperAdmin(user);
|
||||
return this._errorsService.listErrors({
|
||||
page: page ? parseInt(page, 10) : 0,
|
||||
limit: limit ? parseInt(limit, 10) : 20,
|
||||
platform: platform || undefined,
|
||||
email: email || undefined,
|
||||
unknownFirst: unknownFirst === 'true' || unknownFirst === '1',
|
||||
});
|
||||
}
|
||||
|
||||
@Get('/errors/platforms')
|
||||
async listPlatforms(@GetUserFromRequest() user: User) {
|
||||
this.assertSuperAdmin(user);
|
||||
return this._errorsService.listPlatforms();
|
||||
}
|
||||
}
|
||||
37
apps/backend/src/api/routes/agencies.controller.ts
Normal file
37
apps/backend/src/api/routes/agencies.controller.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { User } from '@prisma/client';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AgenciesService } from '@gitroom/nestjs-libraries/database/prisma/agencies/agencies.service';
|
||||
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
|
||||
import { CreateAgencyDto } from '@gitroom/nestjs-libraries/dtos/agencies/create.agency.dto';
|
||||
|
||||
@ApiTags('Agencies')
|
||||
@Controller('/agencies')
|
||||
export class AgenciesController {
|
||||
constructor(private _agenciesService: AgenciesService) {}
|
||||
@Get('/')
|
||||
async getAgencyByUsers(@GetUserFromRequest() user: User) {
|
||||
return (await this._agenciesService.getAgencyByUser(user)) || {};
|
||||
}
|
||||
|
||||
@Post('/')
|
||||
async createAgency(
|
||||
@GetUserFromRequest() user: User,
|
||||
@Body() body: CreateAgencyDto
|
||||
) {
|
||||
return this._agenciesService.createAgency(user, body);
|
||||
}
|
||||
|
||||
@Post('/action/:action/:id')
|
||||
async updateAgency(
|
||||
@GetUserFromRequest() user: User,
|
||||
@Param('action') action: string,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
if (!user.isSuperAdmin) {
|
||||
return 400;
|
||||
}
|
||||
|
||||
return this._agenciesService.approveOrDecline(user.email, action, id);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,56 @@
|
|||
import { Controller, Get, Param, Query } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Inject,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { Organization } from '@prisma/client';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
import { StarsService } from '@gitroom/nestjs-libraries/database/prisma/stars/stars.service';
|
||||
import dayjs from 'dayjs';
|
||||
import { StarsListDto } from '@gitroom/nestjs-libraries/dtos/analytics/stars.list.dto';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
|
||||
@ApiTags('Analytics')
|
||||
@Controller('/analytics')
|
||||
export class AnalyticsController {
|
||||
constructor(
|
||||
private _integrationService: IntegrationService,
|
||||
private _postsService: PostsService
|
||||
private _starsService: StarsService,
|
||||
private _integrationService: IntegrationService
|
||||
) {}
|
||||
@Get('/')
|
||||
async getStars(@GetOrgFromRequest() org: Organization) {
|
||||
return this._starsService.getStars(org.id);
|
||||
}
|
||||
|
||||
@Get('/trending')
|
||||
async getTrending() {
|
||||
const todayTrending = dayjs(dayjs().format('YYYY-MM-DDT12:00:00'));
|
||||
const last = todayTrending.isAfter(dayjs())
|
||||
? todayTrending.subtract(1, 'day')
|
||||
: todayTrending;
|
||||
const nextTrending = last.add(1, 'day');
|
||||
|
||||
return {
|
||||
last: last.format('YYYY-MM-DD HH:mm:ss'),
|
||||
predictions: nextTrending.format('YYYY-MM-DD HH:mm:ss'),
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/stars')
|
||||
async getStarsFilter(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() starsFilter: StarsListDto
|
||||
) {
|
||||
return {
|
||||
stars: await this._starsService.getStarsFilter(org.id, starsFilter),
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/:integration')
|
||||
async getIntegration(
|
||||
|
|
@ -21,13 +60,4 @@ export class AnalyticsController {
|
|||
) {
|
||||
return this._integrationService.checkAnalytics(org, integration, date);
|
||||
}
|
||||
|
||||
@Get('/post/:postId')
|
||||
async getPostAnalytics(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('postId') postId: string,
|
||||
@Query('date') date: string
|
||||
) {
|
||||
return this._postsService.checkPostAnalytics(org.id, postId, +date);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpException,
|
||||
Param,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
|
||||
import { User } from '@prisma/client';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AnnouncementsService } from '@gitroom/nestjs-libraries/database/prisma/announcements/announcements.service';
|
||||
import { AnnouncementDto } from '@gitroom/nestjs-libraries/dtos/announcements/announcements.dto';
|
||||
|
||||
@ApiTags('Announcements')
|
||||
@Controller('/announcements')
|
||||
export class AnnouncementsController {
|
||||
constructor(private _announcementsService: AnnouncementsService) {}
|
||||
|
||||
@Get('/')
|
||||
async getAnnouncements() {
|
||||
return this._announcementsService.getAnnouncements();
|
||||
}
|
||||
|
||||
@Post('/')
|
||||
async createAnnouncement(
|
||||
@GetUserFromRequest() user: User,
|
||||
@Body() body: AnnouncementDto
|
||||
) {
|
||||
if (!user.isSuperAdmin) {
|
||||
throw new HttpException('Unauthorized', 400);
|
||||
}
|
||||
return this._announcementsService.createAnnouncement(body);
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
async deleteAnnouncement(
|
||||
@GetUserFromRequest() user: User,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
if (!user.isSuperAdmin) {
|
||||
throw new HttpException('Unauthorized', 400);
|
||||
}
|
||||
return this._announcementsService.deleteAnnouncement(id);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import { Controller, Delete, Get, Param } from '@nestjs/common';
|
||||
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
|
||||
import { User } from '@prisma/client';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { OAuthService } from '@gitroom/nestjs-libraries/database/prisma/oauth/oauth.service';
|
||||
|
||||
@ApiTags('Approved Apps')
|
||||
@Controller('/user/approved-apps')
|
||||
export class ApprovedAppsController {
|
||||
constructor(private _oauthService: OAuthService) {}
|
||||
|
||||
@Get('/')
|
||||
async list(@GetUserFromRequest() user: User) {
|
||||
return this._oauthService.getApprovedApps(user.id);
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
async revoke(
|
||||
@GetUserFromRequest() user: User,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
return this._oauthService.revokeApp(user.id, id);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@ import { LoginUserDto } from '@gitroom/nestjs-libraries/dtos/auth/login.user.dto
|
|||
import { AuthService } from '@gitroom/backend/services/auth/auth.service';
|
||||
import { ForgotReturnPasswordDto } from '@gitroom/nestjs-libraries/dtos/auth/forgot-return.password.dto';
|
||||
import { ForgotPasswordDto } from '@gitroom/nestjs-libraries/dtos/auth/forgot.password.dto';
|
||||
import { ResendActivationDto } from '@gitroom/nestjs-libraries/dtos/auth/resend-activation.dto';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management';
|
||||
import { EmailService } from '@gitroom/nestjs-libraries/services/email.service';
|
||||
|
|
@ -103,7 +102,7 @@ export class AuthController {
|
|||
}
|
||||
}
|
||||
|
||||
Sentry.metrics.count('new_user', 1);
|
||||
Sentry.metrics.count("new_user", 1);
|
||||
response.header('onboarding', 'true');
|
||||
response.status(200).json({
|
||||
register: true,
|
||||
|
|
@ -199,19 +198,6 @@ export class AuthController {
|
|||
};
|
||||
}
|
||||
|
||||
@Get('/oauth-mobile-callback')
|
||||
mobileCallback(
|
||||
@Query('code') code: string,
|
||||
@Query('state') state: string,
|
||||
@Res({ passthrough: false }) response: Response
|
||||
) {
|
||||
const scheme = process.env.MOBILE_APP_SCHEME || 'postiz://auth/callback';
|
||||
const params = new URLSearchParams();
|
||||
if (code) params.set('code', code);
|
||||
if (state) params.set('state', state);
|
||||
return response.redirect(302, `${scheme}?${params.toString()}`);
|
||||
}
|
||||
|
||||
@Get('/oauth/:provider')
|
||||
async oauthLink(@Param('provider') provider: string, @Query() query: any) {
|
||||
return this._authService.oauthLink(provider, query);
|
||||
|
|
@ -220,13 +206,9 @@ export class AuthController {
|
|||
@Post('/activate')
|
||||
async activate(
|
||||
@Body('code') code: string,
|
||||
@Body('datafast_visitor_id') datafast_visitor_id: string,
|
||||
@Res({ passthrough: false }) response: Response
|
||||
) {
|
||||
const activate = await this._authService.activate(
|
||||
code,
|
||||
datafast_visitor_id
|
||||
);
|
||||
const activate = await this._authService.activate(code);
|
||||
if (!activate) {
|
||||
return response.status(200).json({ can: false });
|
||||
}
|
||||
|
|
@ -252,33 +234,13 @@ export class AuthController {
|
|||
return response.status(200).json({ can: true });
|
||||
}
|
||||
|
||||
@Post('/resend-activation')
|
||||
async resendActivation(@Body() body: ResendActivationDto) {
|
||||
try {
|
||||
await this._authService.resendActivationEmail(body.email);
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
} catch (e: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: e.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Post('/oauth/:provider/exists')
|
||||
async oauthExists(
|
||||
@Body('code') code: string,
|
||||
@Body('redirect_uri') redirect_uri: string,
|
||||
@Param('provider') provider: string,
|
||||
@Res({ passthrough: false }) response: Response
|
||||
) {
|
||||
const { jwt, token } = await this._authService.checkExists(
|
||||
provider,
|
||||
code,
|
||||
redirect_uri
|
||||
);
|
||||
const { jwt, token } = await this._authService.checkExists(provider, code);
|
||||
|
||||
if (token) {
|
||||
return response.json({ token });
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permis
|
|||
import { AutopostService } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.service';
|
||||
import { AutopostDto } from '@gitroom/nestjs-libraries/dtos/autopost/autopost.dto';
|
||||
import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
import { OnlyURL } from '@gitroom/nestjs-libraries/dtos/webhooks/webhooks.dto';
|
||||
|
||||
@ApiTags('Autopost')
|
||||
@Controller('/autopost')
|
||||
|
|
@ -63,7 +62,7 @@ export class AutopostController {
|
|||
}
|
||||
|
||||
@Post('/send')
|
||||
async sendWebhook(@Query() query: OnlyURL) {
|
||||
return this._autopostsService.loadXML(query.url);
|
||||
async sendWebhook(@Query('url') url: string) {
|
||||
return this._autopostsService.loadXML(url);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Body, Controller, Get, HttpException, Param, Post, Req } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Param, Post, Req } from '@nestjs/common';
|
||||
import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
|
||||
import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
|
|
@ -9,7 +9,6 @@ import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.req
|
|||
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
|
||||
import { Request } from 'express';
|
||||
import { Nowpayments } from '@gitroom/nestjs-libraries/crypto/nowpayments';
|
||||
import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
||||
|
||||
@ApiTags('Billing')
|
||||
@Controller('/billing')
|
||||
|
|
@ -31,20 +30,6 @@ export class BillingController {
|
|||
};
|
||||
}
|
||||
|
||||
@Get('/check-discount')
|
||||
async checkDiscount(@GetOrgFromRequest() org: Organization) {
|
||||
return {
|
||||
offerCoupon: !(await this._stripeService.checkDiscount(org.paymentId))
|
||||
? false
|
||||
: AuthService.signJWT({ discount: true }),
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/apply-discount')
|
||||
async applyDiscount(@GetOrgFromRequest() org: Organization) {
|
||||
await this._stripeService.applyDiscount(org.paymentId);
|
||||
}
|
||||
|
||||
@Post('/finish-trial')
|
||||
async finishTrial(@GetOrgFromRequest() org: Organization) {
|
||||
try {
|
||||
|
|
@ -62,23 +47,6 @@ export class BillingController {
|
|||
};
|
||||
}
|
||||
|
||||
@Post('/embedded')
|
||||
embedded(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@GetUserFromRequest() user: User,
|
||||
@Body() body: BillingSubscribeDto,
|
||||
@Req() req: Request
|
||||
) {
|
||||
const uniqueId = req?.cookies?.track;
|
||||
return this._stripeService.embedded(
|
||||
uniqueId,
|
||||
org.id,
|
||||
user.id,
|
||||
body,
|
||||
org.allowTrial
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/subscribe')
|
||||
subscribe(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
|
|
@ -144,43 +112,6 @@ export class BillingController {
|
|||
return this._stripeService.lifetimeDeal(org.id, body.code);
|
||||
}
|
||||
|
||||
@Get('/charges')
|
||||
async getCharges(
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() org: Organization
|
||||
) {
|
||||
if (!user.isSuperAdmin) {
|
||||
throw new HttpException('Unauthorized', 400);
|
||||
}
|
||||
|
||||
return this._stripeService.getCharges(org.id);
|
||||
}
|
||||
|
||||
@Post('/refund-charges')
|
||||
async refundCharges(
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: { chargeIds: string[] }
|
||||
) {
|
||||
if (!user.isSuperAdmin) {
|
||||
throw new HttpException('Unauthorized', 400);
|
||||
}
|
||||
|
||||
return this._stripeService.refundCharges(org.id, body.chargeIds);
|
||||
}
|
||||
|
||||
@Post('/cancel-subscription')
|
||||
async cancelSubscription(
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() org: Organization
|
||||
) {
|
||||
if (!user.isSuperAdmin) {
|
||||
throw new HttpException('Unauthorized', 400);
|
||||
}
|
||||
|
||||
return this._stripeService.cancelSubscription(org.id);
|
||||
}
|
||||
|
||||
@Post('/add-subscription')
|
||||
async addSubscription(
|
||||
@Body() body: { subscription: string },
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/s
|
|||
import { MastraAgent } from '@ag-ui/mastra';
|
||||
import { MastraService } from '@gitroom/nestjs-libraries/chat/mastra.service';
|
||||
import { Request, Response } from 'express';
|
||||
import { RequestContext } from '@mastra/core/di';
|
||||
import { RuntimeContext } from '@mastra/core/di';
|
||||
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
|
||||
import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
|
||||
|
|
@ -72,19 +72,20 @@ export class CopilotController {
|
|||
return;
|
||||
}
|
||||
const mastra = await this._mastraService.mastra();
|
||||
const requestContext = new RequestContext<ChannelsContext>();
|
||||
requestContext.set(
|
||||
const runtimeContext = new RuntimeContext<ChannelsContext>();
|
||||
runtimeContext.set(
|
||||
'integrations',
|
||||
req?.body?.variables?.properties?.integrations || []
|
||||
);
|
||||
|
||||
requestContext.set('organization', JSON.stringify(organization));
|
||||
requestContext.set('ui', 'true');
|
||||
runtimeContext.set('organization', JSON.stringify(organization));
|
||||
runtimeContext.set('ui', 'true');
|
||||
|
||||
const agents = MastraAgent.getLocalAgents({
|
||||
resourceId: organization.id,
|
||||
mastra,
|
||||
requestContext: requestContext as any,
|
||||
// @ts-ignore
|
||||
runtimeContext,
|
||||
});
|
||||
|
||||
const runtime = new CopilotRuntime({
|
||||
|
|
@ -123,7 +124,7 @@ export class CopilotController {
|
|||
const mastra = await this._mastraService.mastra();
|
||||
const memory = await mastra.getAgent('postiz').getMemory();
|
||||
try {
|
||||
return await memory.recall({
|
||||
return await memory.query({
|
||||
resourceId: organization.id,
|
||||
threadId,
|
||||
});
|
||||
|
|
@ -136,12 +137,14 @@ export class CopilotController {
|
|||
@CheckPolicies([AuthorizationActions.Create, Sections.AI])
|
||||
async getList(@GetOrgFromRequest() organization: Organization) {
|
||||
const mastra = await this._mastraService.mastra();
|
||||
// @ts-ignore
|
||||
const memory = await mastra.getAgent('postiz').getMemory();
|
||||
const list = await memory.listThreads({
|
||||
filter: { resourceId: organization.id },
|
||||
const list = await memory.getThreadsByResourceIdPaginated({
|
||||
resourceId: organization.id,
|
||||
perPage: 100000,
|
||||
page: 0,
|
||||
orderBy: { field: 'createdAt', direction: 'DESC' },
|
||||
orderBy: 'createdAt',
|
||||
sortDirection: 'DESC',
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,128 +0,0 @@
|
|||
import { Body, Controller, Param, Post, Res } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
||||
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
|
||||
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
|
||||
@ApiTags('Enterprise')
|
||||
@Controller('/enterprise')
|
||||
export class EnterpriseController {
|
||||
constructor(
|
||||
private _integrationManager: IntegrationManager,
|
||||
private _organizationService: OrganizationService,
|
||||
private _integrationService: IntegrationService,
|
||||
private _postsService: PostsService
|
||||
) {}
|
||||
|
||||
@Post('/create-user')
|
||||
async createUser(@Body('params') params: string) {
|
||||
try {
|
||||
const { id, name, saasName, email } = AuthService.verifyJWT(params) as {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
saasName: string;
|
||||
};
|
||||
|
||||
try {
|
||||
return await this._organizationService.createMaxUser(
|
||||
id,
|
||||
name,
|
||||
saasName,
|
||||
email
|
||||
);
|
||||
} catch (err) {
|
||||
return { create: false };
|
||||
}
|
||||
} catch (err) {
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
|
||||
@Post('/url')
|
||||
async redirectParams(@Body('params') params: string) {
|
||||
try {
|
||||
const load = AuthService.verifyJWT(params) as {
|
||||
redirectUrl: string;
|
||||
apiKey: string;
|
||||
refreshId?: string;
|
||||
provider: string;
|
||||
webhookUrl: string;
|
||||
};
|
||||
|
||||
if (!load || !load.redirectUrl || !load.apiKey || !load.provider) {
|
||||
return;
|
||||
}
|
||||
|
||||
const org = await this._organizationService.getOrgByApiKey(load.apiKey);
|
||||
|
||||
if (!org) {
|
||||
throw new Error('Organization not found');
|
||||
}
|
||||
|
||||
if (
|
||||
!this._integrationManager
|
||||
.getAllowedSocialsIntegrations()
|
||||
.includes(load.provider)
|
||||
) {
|
||||
throw new Error('Integration not allowed');
|
||||
}
|
||||
|
||||
const integrationProvider = this._integrationManager.getSocialIntegration(
|
||||
load.provider
|
||||
);
|
||||
|
||||
const { codeVerifier, state, url } =
|
||||
await integrationProvider.generateAuthUrl();
|
||||
|
||||
if (load.refreshId) {
|
||||
await ioRedis.set(`refresh:${state}`, load.refreshId, 'EX', 3600);
|
||||
}
|
||||
|
||||
await ioRedis.set(`webhookUrl:${state}`, load.webhookUrl, 'EX', 3600);
|
||||
await ioRedis.set(`redirect:${state}`, load.redirectUrl, 'EX', 3600);
|
||||
await ioRedis.set(`organization:${state}`, org.id, 'EX', 3600);
|
||||
await ioRedis.set(`login:${state}`, codeVerifier, 'EX', 3600);
|
||||
|
||||
return url;
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
@Post('/delete-channel')
|
||||
async deleteChannel(@Body('params') params: string) {
|
||||
try {
|
||||
const load = AuthService.verifyJWT(params) as {
|
||||
apiKey: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
if (!load || !load.apiKey || !load.id) {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const org = await this._organizationService.getOrgByApiKey(load.apiKey);
|
||||
|
||||
if (!org) {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const isTherePosts = await this._integrationService.getPostsForChannel(
|
||||
org.id,
|
||||
load.id
|
||||
);
|
||||
if (isTherePosts.length) {
|
||||
for (const post of isTherePosts) {
|
||||
this._postsService.deletePost(org.id, post.group).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
await this._integrationService.deleteChannel(org.id, load.id);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,12 +3,15 @@ import {
|
|||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpException,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseFilters,
|
||||
} from '@nestjs/common';
|
||||
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
|
||||
import { ConnectIntegrationDto } from '@gitroom/nestjs-libraries/dtos/integrations/connect.integration.dto';
|
||||
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
|
|
@ -18,20 +21,23 @@ import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permis
|
|||
import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
|
||||
import { NotEnoughScopesFilter } from '@gitroom/nestjs-libraries/integrations/integration.missing.scopes';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.time.dto';
|
||||
import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
||||
import { AuthTokenDetails } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { PlugDto } from '@gitroom/nestjs-libraries/dtos/plugs/plug.dto';
|
||||
import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
|
||||
import {
|
||||
NotEnoughScopes,
|
||||
RefreshToken,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { TelegramProvider } from '@gitroom/nestjs-libraries/integrations/social/telegram.provider';
|
||||
import { MoltbookProvider } from '@gitroom/nestjs-libraries/integrations/social/moltbook.provider';
|
||||
import {
|
||||
AuthorizationActions,
|
||||
Sections,
|
||||
} from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
import { uniqBy } from 'lodash';
|
||||
import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service';
|
||||
|
||||
@ApiTags('Integrations')
|
||||
@Controller('/integrations')
|
||||
|
|
@ -39,18 +45,11 @@ export class IntegrationsController {
|
|||
constructor(
|
||||
private _integrationManager: IntegrationManager,
|
||||
private _integrationService: IntegrationService,
|
||||
private _postService: PostsService,
|
||||
private _refreshIntegrationService: RefreshIntegrationService
|
||||
private _postService: PostsService
|
||||
) {}
|
||||
|
||||
@Post('/provider/:id/connect')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.CHANNEL])
|
||||
async saveProviderPage(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string,
|
||||
@Body() body: any
|
||||
) {
|
||||
return this._integrationService.saveProviderPage(org.id, id, body);
|
||||
@Get('/')
|
||||
getIntegration() {
|
||||
return this._integrationManager.getAllIntegrations();
|
||||
}
|
||||
|
||||
@Get('/:identifier/internal-plugs')
|
||||
|
|
@ -101,7 +100,6 @@ export class IntegrationsController {
|
|||
internalId: p.internalId,
|
||||
disabled: p.disabled,
|
||||
editor: findIntegration.editor,
|
||||
stripLinks: !!findIntegration?.stripLinks?.(),
|
||||
picture: p.picture || '/no-picture.jpg',
|
||||
identifier: p.providerIdentifier,
|
||||
inBetweenSteps: p.inBetweenSteps,
|
||||
|
|
@ -195,10 +193,7 @@ export class IntegrationsController {
|
|||
async getIntegrationUrl(
|
||||
@Param('integration') integration: string,
|
||||
@Query('refresh') refresh: string,
|
||||
@Query('externalUrl') externalUrl: string,
|
||||
@Query('redirectUrl') redirectUrl: string,
|
||||
@Query('onboarding') onboarding: string,
|
||||
@GetOrgFromRequest() org: Organization
|
||||
@Query('externalUrl') externalUrl: string
|
||||
) {
|
||||
if (
|
||||
!this._integrationManager
|
||||
|
|
@ -227,24 +222,15 @@ export class IntegrationsController {
|
|||
await integrationProvider.generateAuthUrl(getExternalUrl);
|
||||
|
||||
if (refresh) {
|
||||
await ioRedis.set(`refresh:${state}`, refresh, 'EX', 3600);
|
||||
await ioRedis.set(`refresh:${state}`, refresh, 'EX', 300);
|
||||
}
|
||||
|
||||
if (onboarding === 'true') {
|
||||
await ioRedis.set(`onboarding:${state}`, 'true', 'EX', 3600);
|
||||
}
|
||||
|
||||
if (redirectUrl) {
|
||||
await ioRedis.set(`redirect:${state}`, redirectUrl, 'EX', 3600);
|
||||
}
|
||||
|
||||
await ioRedis.set(`organization:${state}`, org.id, 'EX', 3600);
|
||||
await ioRedis.set(`login:${state}`, codeVerifier, 'EX', 3600);
|
||||
await ioRedis.set(`login:${state}`, codeVerifier, 'EX', 300);
|
||||
await ioRedis.set(
|
||||
`external:${state}`,
|
||||
JSON.stringify(getExternalUrl),
|
||||
'EX',
|
||||
3600
|
||||
300
|
||||
);
|
||||
|
||||
return { url };
|
||||
|
|
@ -352,24 +338,37 @@ export class IntegrationsController {
|
|||
return load;
|
||||
} catch (err) {
|
||||
if (err instanceof RefreshToken) {
|
||||
const data = await this._refreshIntegrationService.refresh(
|
||||
getIntegration
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { accessToken } = data;
|
||||
const { accessToken, refreshToken, expiresIn, additionalSettings } =
|
||||
await integrationProvider.refreshToken(getIntegration.refreshToken);
|
||||
|
||||
if (accessToken) {
|
||||
await this._integrationService.createOrUpdateIntegration(
|
||||
additionalSettings,
|
||||
!!integrationProvider.oneTimeToken,
|
||||
getIntegration.organizationId,
|
||||
getIntegration.name,
|
||||
getIntegration.picture!,
|
||||
'social',
|
||||
getIntegration.internalId,
|
||||
getIntegration.providerIdentifier,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn
|
||||
);
|
||||
|
||||
getIntegration.token = accessToken;
|
||||
|
||||
if (integrationProvider.refreshWait) {
|
||||
await timer(10000);
|
||||
}
|
||||
return this.functionIntegration(org, body);
|
||||
} else {
|
||||
await this._integrationService.disconnectChannel(
|
||||
org.id,
|
||||
getIntegration
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
@ -378,6 +377,154 @@ export class IntegrationsController {
|
|||
throw new Error('Function not found');
|
||||
}
|
||||
|
||||
@Post('/social/:integration/connect')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.CHANNEL])
|
||||
@UseFilters(new NotEnoughScopesFilter())
|
||||
async connectSocialMedia(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('integration') integration: string,
|
||||
@Body() body: ConnectIntegrationDto
|
||||
) {
|
||||
if (
|
||||
!this._integrationManager
|
||||
.getAllowedSocialsIntegrations()
|
||||
.includes(integration)
|
||||
) {
|
||||
throw new Error('Integration not allowed');
|
||||
}
|
||||
|
||||
const integrationProvider =
|
||||
this._integrationManager.getSocialIntegration(integration);
|
||||
|
||||
const getCodeVerifier = integrationProvider.customFields
|
||||
? 'none'
|
||||
: await ioRedis.get(`login:${body.state}`);
|
||||
if (!getCodeVerifier) {
|
||||
throw new Error('Invalid state');
|
||||
}
|
||||
|
||||
if (!integrationProvider.customFields) {
|
||||
await ioRedis.del(`login:${body.state}`);
|
||||
}
|
||||
|
||||
const details = integrationProvider.externalUrl
|
||||
? await ioRedis.get(`external:${body.state}`)
|
||||
: undefined;
|
||||
|
||||
if (details) {
|
||||
await ioRedis.del(`external:${body.state}`);
|
||||
}
|
||||
|
||||
const refresh = await ioRedis.get(`refresh:${body.state}`);
|
||||
if (refresh) {
|
||||
await ioRedis.del(`refresh:${body.state}`);
|
||||
}
|
||||
|
||||
const {
|
||||
error,
|
||||
accessToken,
|
||||
expiresIn,
|
||||
refreshToken,
|
||||
id,
|
||||
name,
|
||||
picture,
|
||||
username,
|
||||
additionalSettings,
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
} = await new Promise<AuthTokenDetails>(async (res) => {
|
||||
const auth = await integrationProvider.authenticate(
|
||||
{
|
||||
code: body.code,
|
||||
codeVerifier: getCodeVerifier,
|
||||
refresh: body.refresh,
|
||||
},
|
||||
details ? JSON.parse(details) : undefined
|
||||
);
|
||||
|
||||
if (typeof auth === 'string') {
|
||||
return res({
|
||||
error: auth,
|
||||
accessToken: '',
|
||||
id: '',
|
||||
name: '',
|
||||
picture: '',
|
||||
username: '',
|
||||
additionalSettings: [],
|
||||
});
|
||||
}
|
||||
|
||||
if (refresh && integrationProvider.reConnect) {
|
||||
const newAuth = await integrationProvider.reConnect(
|
||||
auth.id,
|
||||
refresh,
|
||||
auth.accessToken
|
||||
);
|
||||
return res(newAuth);
|
||||
}
|
||||
|
||||
return res(auth);
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new NotEnoughScopes(error);
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
throw new NotEnoughScopes('Invalid API key');
|
||||
}
|
||||
|
||||
if (refresh && String(id) !== String(refresh)) {
|
||||
throw new NotEnoughScopes(
|
||||
'Please refresh the channel that needs to be refreshed'
|
||||
);
|
||||
}
|
||||
|
||||
let validName = name;
|
||||
if (!validName) {
|
||||
if (username) {
|
||||
validName = username.split('.')[0] ?? username;
|
||||
} else {
|
||||
validName = `Channel_${String(id).slice(0, 8)}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
process.env.STRIPE_PUBLISHABLE_KEY &&
|
||||
org.isTrailing &&
|
||||
(await this._integrationService.checkPreviousConnections(
|
||||
org.id,
|
||||
String(id)
|
||||
))
|
||||
) {
|
||||
throw new HttpException('', 412);
|
||||
}
|
||||
|
||||
return this._integrationService.createOrUpdateIntegration(
|
||||
additionalSettings,
|
||||
!!integrationProvider.oneTimeToken,
|
||||
org.id,
|
||||
validName.trim(),
|
||||
picture,
|
||||
'social',
|
||||
String(id),
|
||||
integration,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn,
|
||||
username,
|
||||
refresh ? false : integrationProvider.isBetweenSteps,
|
||||
body.refresh,
|
||||
+body.timezone,
|
||||
details
|
||||
? AuthService.fixedEncryption(details)
|
||||
: integrationProvider.customFields
|
||||
? AuthService.fixedEncryption(
|
||||
Buffer.from(body.code, 'base64').toString()
|
||||
)
|
||||
: undefined
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/disable')
|
||||
disableChannel(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
|
|
@ -386,6 +533,33 @@ export class IntegrationsController {
|
|||
return this._integrationService.disableChannel(org.id, id);
|
||||
}
|
||||
|
||||
@Post('/instagram/:id')
|
||||
async saveInstagram(
|
||||
@Param('id') id: string,
|
||||
@Body() body: { pageId: string; id: string },
|
||||
@GetOrgFromRequest() org: Organization
|
||||
) {
|
||||
return this._integrationService.saveInstagram(org.id, id, body);
|
||||
}
|
||||
|
||||
@Post('/facebook/:id')
|
||||
async saveFacebook(
|
||||
@Param('id') id: string,
|
||||
@Body() body: { page: string },
|
||||
@GetOrgFromRequest() org: Organization
|
||||
) {
|
||||
return this._integrationService.saveFacebook(org.id, id, body.page);
|
||||
}
|
||||
|
||||
@Post('/linkedin-page/:id')
|
||||
async saveLinkedin(
|
||||
@Param('id') id: string,
|
||||
@Body() body: { page: string },
|
||||
@GetOrgFromRequest() org: Organization
|
||||
) {
|
||||
return this._integrationService.saveLinkedin(org.id, id, body.page);
|
||||
}
|
||||
|
||||
@Post('/enable')
|
||||
enableChannel(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
|
|
@ -410,7 +584,7 @@ export class IntegrationsController {
|
|||
);
|
||||
if (isTherePosts.length) {
|
||||
for (const post of isTherePosts) {
|
||||
this._postService.deletePost(org.id, post.group).catch((err) => {});
|
||||
await this._postService.deletePost(org.id, post.group);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -452,30 +626,4 @@ export class IntegrationsController {
|
|||
async getUpdates(@Query() query: { word: string; id?: number }) {
|
||||
return new TelegramProvider().getBotId(query);
|
||||
}
|
||||
|
||||
@Post('/moltbook/register')
|
||||
async moltbookRegister(@Body() body: { name: string; description: string }) {
|
||||
try {
|
||||
const provider = new MoltbookProvider();
|
||||
const result = await provider.registerAgent(body.name, body.description);
|
||||
return {
|
||||
apiKey: result.api_key,
|
||||
claimUrl: result.claim_url,
|
||||
verificationCode: result.verification_code,
|
||||
};
|
||||
} catch (err: any) {
|
||||
return { error: err.message || 'Registration failed' };
|
||||
}
|
||||
}
|
||||
|
||||
@Get('/moltbook/status')
|
||||
async moltbookStatus(@Query('apiKey') apiKey: string) {
|
||||
try {
|
||||
const provider = new MoltbookProvider();
|
||||
const result = await provider.checkAgentStatus(apiKey);
|
||||
return { claimed: result?.status === 'claimed' };
|
||||
} catch (err) {
|
||||
return { claimed: false };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
242
apps/backend/src/api/routes/marketplace.controller.ts
Normal file
242
apps/backend/src/api/routes/marketplace.controller.ts
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
|
||||
import { Organization, User } from '@prisma/client';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
|
||||
import { ItemUserService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/item.user.service';
|
||||
import { AddRemoveItemDto } from '@gitroom/nestjs-libraries/dtos/marketplace/add.remove.item.dto';
|
||||
import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service';
|
||||
import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service';
|
||||
import { ChangeActiveDto } from '@gitroom/nestjs-libraries/dtos/marketplace/change.active.dto';
|
||||
import { ItemsDto } from '@gitroom/nestjs-libraries/dtos/marketplace/items.dto';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
import { AudienceDto } from '@gitroom/nestjs-libraries/dtos/marketplace/audience.dto';
|
||||
import { NewConversationDto } from '@gitroom/nestjs-libraries/dtos/marketplace/new.conversation.dto';
|
||||
import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service';
|
||||
import { CreateOfferDto } from '@gitroom/nestjs-libraries/dtos/marketplace/create.offer.dto';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
|
||||
@ApiTags('Marketplace')
|
||||
@Controller('/marketplace')
|
||||
export class MarketplaceController {
|
||||
constructor(
|
||||
private _itemUserService: ItemUserService,
|
||||
private _stripeService: StripeService,
|
||||
private _userService: UsersService,
|
||||
private _messagesService: MessagesService,
|
||||
private _postsService: PostsService
|
||||
) {}
|
||||
|
||||
@Post('/')
|
||||
getInfluencers(
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@GetUserFromRequest() user: User,
|
||||
@Body() body: ItemsDto
|
||||
) {
|
||||
return this._userService.getMarketplacePeople(
|
||||
organization.id,
|
||||
user.id,
|
||||
body
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/conversation')
|
||||
createConversation(
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Body() body: NewConversationDto
|
||||
) {
|
||||
return this._messagesService.createConversation(
|
||||
user.id,
|
||||
organization.id,
|
||||
body
|
||||
);
|
||||
}
|
||||
|
||||
@Get('/bank')
|
||||
connectBankAccount(
|
||||
@GetUserFromRequest() user: User,
|
||||
@Query('country') country: string
|
||||
) {
|
||||
return this._stripeService.createAccountProcess(
|
||||
user.id,
|
||||
user.email,
|
||||
country
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/item')
|
||||
async addItems(
|
||||
@GetUserFromRequest() user: User,
|
||||
@Body() body: AddRemoveItemDto
|
||||
) {
|
||||
return this._itemUserService.addOrRemoveItem(body.state, user.id, body.key);
|
||||
}
|
||||
|
||||
@Post('/active')
|
||||
async changeActive(
|
||||
@GetUserFromRequest() user: User,
|
||||
@Body() body: ChangeActiveDto
|
||||
) {
|
||||
await this._userService.changeMarketplaceActive(user.id, body.active);
|
||||
}
|
||||
|
||||
@Post('/audience')
|
||||
async changeAudience(
|
||||
@GetUserFromRequest() user: User,
|
||||
@Body() body: AudienceDto
|
||||
) {
|
||||
await this._userService.changeAudienceSize(user.id, body.audience);
|
||||
}
|
||||
|
||||
@Get('/item')
|
||||
async getItems(@GetUserFromRequest() user: User) {
|
||||
return this._itemUserService.getItems(user.id);
|
||||
}
|
||||
|
||||
@Get('/orders')
|
||||
async getOrders(
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Query('type') type: 'seller' | 'buyer'
|
||||
) {
|
||||
return this._messagesService.getOrders(user.id, organization.id, type);
|
||||
}
|
||||
|
||||
@Get('/account')
|
||||
async getAccount(@GetUserFromRequest() user: User) {
|
||||
const { account, marketplace, connectedAccount, name, picture, audience } =
|
||||
await this._userService.getUserByEmail(user.email);
|
||||
return {
|
||||
account,
|
||||
marketplace,
|
||||
connectedAccount,
|
||||
fullname: name,
|
||||
audience,
|
||||
picture,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/offer')
|
||||
async createOffer(
|
||||
@GetUserFromRequest() user: User,
|
||||
@Body() body: CreateOfferDto
|
||||
) {
|
||||
return this._messagesService.createOffer(user.id, body);
|
||||
}
|
||||
|
||||
@Get('/posts/:id')
|
||||
async post(
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
const getPost = await this._messagesService.getPost(
|
||||
user.id,
|
||||
organization.id,
|
||||
id
|
||||
);
|
||||
if (!getPost) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
...(await this._postsService.getPost(getPost.organizationId, id)),
|
||||
providerId: getPost.integration.providerIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/posts/:id/revision')
|
||||
async revision(
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Param('id') id: string,
|
||||
@Body('message') message: string
|
||||
) {
|
||||
return this._messagesService.requestRevision(
|
||||
user.id,
|
||||
organization.id,
|
||||
id,
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/posts/:id/approve')
|
||||
async approve(
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Param('id') id: string,
|
||||
@Body('message') message: string
|
||||
) {
|
||||
return this._messagesService.requestApproved(
|
||||
user.id,
|
||||
organization.id,
|
||||
id,
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/posts/:id/cancel')
|
||||
async cancel(
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
return this._messagesService.requestCancel(organization.id, id);
|
||||
}
|
||||
|
||||
@Post('/offer/:id/complete')
|
||||
async completeOrder(
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
const order = await this._messagesService.completeOrderAndPay(
|
||||
organization.id,
|
||||
id
|
||||
);
|
||||
|
||||
if (!order) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this._stripeService.payout(
|
||||
id,
|
||||
order.charge,
|
||||
order.account,
|
||||
order.price
|
||||
);
|
||||
} catch (e) {
|
||||
await this._messagesService.payoutProblem(
|
||||
id,
|
||||
order.sellerId,
|
||||
order.price
|
||||
);
|
||||
}
|
||||
await this._messagesService.completeOrder(id);
|
||||
}
|
||||
|
||||
@Post('/orders/:id/payment')
|
||||
async payOrder(
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
const orderDetails = await this._messagesService.getOrderDetails(
|
||||
user.id,
|
||||
organization.id,
|
||||
id
|
||||
);
|
||||
const payment = await this._stripeService.payAccountStepOne(
|
||||
user.id,
|
||||
organization,
|
||||
orderDetails.seller,
|
||||
orderDetails.order.id,
|
||||
orderDetails.order.ordersItems.map((p) => ({
|
||||
quantity: p.quantity,
|
||||
integrationType: p.integration.providerIdentifier,
|
||||
price: p.price,
|
||||
})),
|
||||
orderDetails.order.messageGroupId
|
||||
);
|
||||
return payment;
|
||||
}
|
||||
}
|
||||
|
|
@ -91,13 +91,11 @@ export class MediaController {
|
|||
@GetOrgFromRequest() org: Organization,
|
||||
@UploadedFile() file: Express.Multer.File
|
||||
) {
|
||||
const originalName = file?.originalname || '';
|
||||
const uploadedFile = await this.storage.uploadFile(file);
|
||||
return this._mediaService.saveFile(
|
||||
org.id,
|
||||
uploadedFile.originalname,
|
||||
uploadedFile.path,
|
||||
originalName
|
||||
uploadedFile.path
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -105,8 +103,7 @@ export class MediaController {
|
|||
async saveMedia(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Req() req: Request,
|
||||
@Body('name') name: string,
|
||||
@Body('originalName') originalName: string
|
||||
@Body('name') name: string
|
||||
) {
|
||||
if (!name) {
|
||||
return false;
|
||||
|
|
@ -114,8 +111,7 @@ export class MediaController {
|
|||
return this._mediaService.saveFile(
|
||||
org.id,
|
||||
name,
|
||||
process.env.CLOUDFLARE_BUCKET_URL + '/' + name,
|
||||
originalName || undefined
|
||||
process.env.CLOUDFLARE_BUCKET_URL + '/' + name
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -129,13 +125,11 @@ export class MediaController {
|
|||
|
||||
@Post('/upload-simple')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
@UsePipes(new CustomFileValidationPipe())
|
||||
async uploadSimple(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@UploadedFile('file') file: Express.Multer.File,
|
||||
@Body('preventSave') preventSave: string = 'false'
|
||||
) {
|
||||
const originalName = file.originalname;
|
||||
const getFile = await this.storage.uploadFile(file);
|
||||
|
||||
if (preventSave === 'true') {
|
||||
|
|
@ -146,8 +140,7 @@ export class MediaController {
|
|||
return this._mediaService.saveFile(
|
||||
org.id,
|
||||
getFile.originalname,
|
||||
getFile.path,
|
||||
originalName
|
||||
getFile.path
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -165,14 +158,12 @@ export class MediaController {
|
|||
|
||||
// @ts-ignore
|
||||
const name = upload.Location.split('/').pop();
|
||||
const originalName = req.body?.file?.name;
|
||||
|
||||
const saveFile = await this._mediaService.saveFile(
|
||||
org.id,
|
||||
name,
|
||||
// @ts-ignore
|
||||
upload.Location,
|
||||
originalName || undefined
|
||||
upload.Location
|
||||
);
|
||||
|
||||
res.status(200).json({ ...upload, saved: saveFile });
|
||||
|
|
@ -181,10 +172,9 @@ export class MediaController {
|
|||
@Get('/')
|
||||
getMedia(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Query('page') page: number,
|
||||
@Query('search') search?: string
|
||||
@Query('page') page: number
|
||||
) {
|
||||
return this._mediaService.getMedia(org.id, page, search);
|
||||
return this._mediaService.getMedia(org.id, page);
|
||||
}
|
||||
|
||||
@Get('/video-options')
|
||||
|
|
|
|||
50
apps/backend/src/api/routes/messages.controller.ts
Normal file
50
apps/backend/src/api/routes/messages.controller.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service';
|
||||
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
|
||||
import { Organization, User } from '@prisma/client';
|
||||
import { AddMessageDto } from '@gitroom/nestjs-libraries/dtos/messages/add.message';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
|
||||
@ApiTags('Messages')
|
||||
@Controller('/messages')
|
||||
export class MessagesController {
|
||||
constructor(private _messagesService: MessagesService) {}
|
||||
|
||||
@Get('/')
|
||||
getMessagesGroup(
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() organization: Organization
|
||||
) {
|
||||
return this._messagesService.getMessagesGroup(user.id, organization.id);
|
||||
}
|
||||
|
||||
@Get('/:groupId/:page')
|
||||
getMessages(
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Param('groupId') groupId: string,
|
||||
@Param('page') page: string
|
||||
) {
|
||||
return this._messagesService.getMessages(
|
||||
user.id,
|
||||
organization.id,
|
||||
groupId,
|
||||
+page
|
||||
);
|
||||
}
|
||||
@Post('/:groupId')
|
||||
createMessage(
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Param('groupId') groupId: string,
|
||||
@Body() message: AddMessageDto
|
||||
) {
|
||||
return this._messagesService.createMessage(
|
||||
user.id,
|
||||
organization.id,
|
||||
groupId,
|
||||
message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,30 @@
|
|||
import { Controller, Get, Param } from '@nestjs/common';
|
||||
import { Controller, Get, HttpException, Param } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
|
||||
@ApiTags('Monitor')
|
||||
@Controller('/monitor')
|
||||
export class MonitorController {
|
||||
constructor(private _workerServiceProducer: BullMqClient) {}
|
||||
|
||||
@Get('/queue/:name')
|
||||
async getMessagesGroup(@Param('name') name: string) {
|
||||
return {
|
||||
status: 'success',
|
||||
message: `Queue ${name} is healthy.`,
|
||||
};
|
||||
const { valid } =
|
||||
await this._workerServiceProducer.checkForStuckWaitingJobs(name);
|
||||
|
||||
if (valid) {
|
||||
return {
|
||||
status: 'success',
|
||||
message: `Queue ${name} is healthy.`,
|
||||
};
|
||||
}
|
||||
|
||||
throw new HttpException(
|
||||
{
|
||||
status: 'error',
|
||||
message: `Queue ${name} has stuck waiting jobs.`,
|
||||
},
|
||||
503
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,395 +0,0 @@
|
|||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpException,
|
||||
Param,
|
||||
Post,
|
||||
UseFilters,
|
||||
} from '@nestjs/common';
|
||||
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
|
||||
import { ConnectIntegrationDto } from '@gitroom/nestjs-libraries/dtos/integrations/connect.integration.dto';
|
||||
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { NotEnoughScopesFilter } from '@gitroom/nestjs-libraries/integrations/integration.missing.scopes';
|
||||
import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
||||
import { AuthTokenDetails } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { NotEnoughScopes } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import {
|
||||
AuthorizationActions,
|
||||
Sections,
|
||||
} from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service';
|
||||
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
|
||||
|
||||
@ApiTags('Integrations')
|
||||
@Controller('/integrations')
|
||||
export class NoAuthIntegrationsController {
|
||||
constructor(
|
||||
private _integrationManager: IntegrationManager,
|
||||
private _integrationService: IntegrationService,
|
||||
private _refreshIntegrationService: RefreshIntegrationService,
|
||||
private _organizationService: OrganizationService
|
||||
) {}
|
||||
|
||||
@Get('/')
|
||||
getIntegrations() {
|
||||
return this._integrationManager.getAllIntegrations();
|
||||
}
|
||||
|
||||
@Post('/social-connect/:integration')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.CHANNEL])
|
||||
@UseFilters(new NotEnoughScopesFilter())
|
||||
async connectSocialMedia(
|
||||
@Param('integration') integration: string,
|
||||
@Body() body: ConnectIntegrationDto
|
||||
) {
|
||||
if (
|
||||
!this._integrationManager
|
||||
.getAllowedSocialsIntegrations()
|
||||
.includes(integration)
|
||||
) {
|
||||
throw new Error('Integration not allowed');
|
||||
}
|
||||
|
||||
const integrationProvider =
|
||||
this._integrationManager.getSocialIntegration(integration);
|
||||
|
||||
const getCodeVerifier = integrationProvider.customFields
|
||||
? 'none'
|
||||
: await ioRedis.get(`login:${body.state}`);
|
||||
if (!getCodeVerifier) {
|
||||
throw new Error('Invalid state');
|
||||
}
|
||||
|
||||
const organization = await ioRedis.get(`organization:${body.state}`);
|
||||
if (!organization) {
|
||||
throw new Error('Organization not found');
|
||||
}
|
||||
|
||||
const org = await this._organizationService.getOrgById(organization);
|
||||
|
||||
if (!integrationProvider.customFields) {
|
||||
await ioRedis.del(`login:${body.state}`);
|
||||
}
|
||||
|
||||
const details = integrationProvider.externalUrl
|
||||
? await ioRedis.get(`external:${body.state}`)
|
||||
: undefined;
|
||||
|
||||
if (details) {
|
||||
await ioRedis.del(`external:${body.state}`);
|
||||
}
|
||||
|
||||
const refresh = await ioRedis.get(`refresh:${body.state}`);
|
||||
if (refresh) {
|
||||
await ioRedis.del(`refresh:${body.state}`);
|
||||
}
|
||||
|
||||
const onboarding = await ioRedis.get(`onboarding:${body.state}`);
|
||||
if (onboarding) {
|
||||
await ioRedis.del(`onboarding:${body.state}`);
|
||||
}
|
||||
|
||||
const {
|
||||
error,
|
||||
accessToken,
|
||||
expiresIn,
|
||||
refreshToken,
|
||||
id,
|
||||
name,
|
||||
picture,
|
||||
username,
|
||||
additionalSettings,
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
} = await new Promise<AuthTokenDetails>(async (res) => {
|
||||
try {
|
||||
const auth = await integrationProvider.authenticate(
|
||||
{
|
||||
code: body.code,
|
||||
codeVerifier: getCodeVerifier,
|
||||
refresh: body.refresh,
|
||||
},
|
||||
details ? JSON.parse(details) : undefined
|
||||
);
|
||||
|
||||
if (typeof auth === 'string') {
|
||||
return res({
|
||||
error: auth,
|
||||
accessToken: '',
|
||||
id: '',
|
||||
name: '',
|
||||
picture: '',
|
||||
username: '',
|
||||
additionalSettings: [],
|
||||
});
|
||||
}
|
||||
|
||||
if (refresh && integrationProvider.reConnect) {
|
||||
console.log('reconnect');
|
||||
try {
|
||||
const newAuth = await integrationProvider.reConnect(
|
||||
auth.id,
|
||||
refresh,
|
||||
auth.accessToken
|
||||
);
|
||||
return res({ ...newAuth, refreshToken: body.refresh });
|
||||
} catch (err: any) {
|
||||
return res({
|
||||
error: err.message,
|
||||
accessToken: '',
|
||||
id: '',
|
||||
name: '',
|
||||
picture: '',
|
||||
username: '',
|
||||
additionalSettings: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return res(auth);
|
||||
} catch (err) {
|
||||
if (err instanceof NotEnoughScopes) {
|
||||
return res({
|
||||
error: err.message,
|
||||
accessToken: '',
|
||||
id: '',
|
||||
name: '',
|
||||
picture: '',
|
||||
username: '',
|
||||
additionalSettings: [],
|
||||
});
|
||||
}
|
||||
|
||||
return res({
|
||||
error: 'Authentication failed',
|
||||
accessToken: '',
|
||||
id: '',
|
||||
name: '',
|
||||
picture: '',
|
||||
username: '',
|
||||
additionalSettings: [],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new NotEnoughScopes(error);
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
throw new NotEnoughScopes('Invalid API key');
|
||||
}
|
||||
|
||||
if (refresh && String(id) !== String(refresh)) {
|
||||
throw new NotEnoughScopes(
|
||||
'Please refresh the channel that needs to be refreshed'
|
||||
);
|
||||
}
|
||||
|
||||
let validName = name;
|
||||
if (!validName) {
|
||||
if (username) {
|
||||
validName = username.split('.')[0] ?? username;
|
||||
} else {
|
||||
validName = `Channel_${String(id).slice(0, 8)}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
process.env.STRIPE_PUBLISHABLE_KEY &&
|
||||
org.isTrailing &&
|
||||
(await this._integrationService.checkPreviousConnections(
|
||||
org.id,
|
||||
String(id)
|
||||
))
|
||||
) {
|
||||
throw new HttpException('', 412);
|
||||
}
|
||||
|
||||
const createUpdate =
|
||||
await this._integrationService.createOrUpdateIntegration(
|
||||
additionalSettings,
|
||||
!!integrationProvider.oneTimeToken,
|
||||
org.id,
|
||||
validName.trim(),
|
||||
picture,
|
||||
'social',
|
||||
String(id),
|
||||
integration,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn,
|
||||
username,
|
||||
refresh ? false : integrationProvider.isBetweenSteps,
|
||||
body.refresh,
|
||||
+body.timezone,
|
||||
details
|
||||
? AuthService.fixedEncryption(details)
|
||||
: integrationProvider.customFields
|
||||
? AuthService.fixedEncryption(
|
||||
Buffer.from(body.code, 'base64').toString()
|
||||
)
|
||||
: integrationProvider.isChromeExtension
|
||||
? AuthService.signJWT(
|
||||
JSON.parse(Buffer.from(body.code, 'base64').toString())
|
||||
)
|
||||
: undefined
|
||||
);
|
||||
|
||||
this._refreshIntegrationService
|
||||
.startRefreshWorkflow(org.id, createUpdate.id, integrationProvider)
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
// Fetch pages if this is a two-step provider and not a refresh
|
||||
let pages: any[] = [];
|
||||
if (integrationProvider.isBetweenSteps && !refresh) {
|
||||
try {
|
||||
// Check which method the provider uses (pages or companies)
|
||||
const fetchMethod =
|
||||
'pages' in integrationProvider
|
||||
? 'pages'
|
||||
: 'companies' in integrationProvider
|
||||
? 'companies'
|
||||
: null;
|
||||
|
||||
if (fetchMethod) {
|
||||
// @ts-ignore - dynamic method call
|
||||
pages = await integrationProvider[fetchMethod](accessToken);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Failed to fetch pages:', err);
|
||||
}
|
||||
}
|
||||
|
||||
const webhookUrl = await ioRedis.get(`webhookUrl:${body.state}`);
|
||||
if (webhookUrl) {
|
||||
try {
|
||||
await fetch(webhookUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
params: AuthService.signJWT({
|
||||
apiKey: org.apiKey,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
} catch (err) {}
|
||||
|
||||
await ioRedis.del(`webhookUrl:${body.state}`);
|
||||
}
|
||||
|
||||
const returnURL = await ioRedis.get(`redirect:${body.state}`);
|
||||
if (returnURL) {
|
||||
await ioRedis.del(`redirect:${body.state}`);
|
||||
}
|
||||
|
||||
const extensionToken = integrationProvider.isChromeExtension
|
||||
? AuthService.signJWT({
|
||||
integrationId: createUpdate.id,
|
||||
organizationId: org.id,
|
||||
internalId: String(id),
|
||||
provider: integration,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
...createUpdate,
|
||||
onboarding: onboarding === 'true',
|
||||
pages,
|
||||
...(returnURL ? { returnURL } : {}),
|
||||
...(extensionToken ? { extensionToken } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/public/provider/:id/connect')
|
||||
async saveProviderPage(@Param('id') id: string, @Body() body: any) {
|
||||
if (!body.state) {
|
||||
throw new Error('Invalid state');
|
||||
}
|
||||
|
||||
const organization = await ioRedis.get(`organization:${body.state}`);
|
||||
if (!organization) {
|
||||
throw new Error('Organization not found');
|
||||
}
|
||||
|
||||
const org = await this._organizationService.getOrgById(organization);
|
||||
|
||||
return this._integrationService.saveProviderPage(org.id, id, body);
|
||||
}
|
||||
|
||||
@Post('/extension-refresh')
|
||||
async extensionRefreshCookies(
|
||||
@Body() body: { jwt: string; cookies: string }
|
||||
) {
|
||||
let payload: any;
|
||||
try {
|
||||
payload = AuthService.verifyJWT(body.jwt);
|
||||
} catch {
|
||||
throw new HttpException('Invalid token', 401);
|
||||
}
|
||||
|
||||
const { integrationId, organizationId, internalId, provider } = payload;
|
||||
if (!integrationId || !organizationId || !internalId || !provider) {
|
||||
throw new HttpException('Invalid token payload', 400);
|
||||
}
|
||||
|
||||
const integration = await this._integrationService.getIntegrationById(
|
||||
organizationId,
|
||||
integrationId
|
||||
);
|
||||
if (!integration || integration.internalId !== internalId) {
|
||||
throw new HttpException('Integration not found', 404);
|
||||
}
|
||||
|
||||
const integrationProvider =
|
||||
this._integrationManager.getSocialIntegration(provider);
|
||||
if (!integrationProvider?.isChromeExtension) {
|
||||
throw new HttpException('Not a Chrome extension integration', 400);
|
||||
}
|
||||
|
||||
const authResult = await integrationProvider.authenticate({
|
||||
code: body.cookies,
|
||||
codeVerifier: '',
|
||||
});
|
||||
|
||||
if (typeof authResult === 'string') {
|
||||
throw new HttpException(authResult, 400);
|
||||
}
|
||||
|
||||
if (String(authResult.id) !== String(integration.internalId)) {
|
||||
await this._integrationService.refreshNeeded(
|
||||
organizationId,
|
||||
integrationId
|
||||
);
|
||||
return { success: false, reason: 'account_mismatch' };
|
||||
}
|
||||
|
||||
await this._integrationService.createOrUpdateIntegration(
|
||||
undefined,
|
||||
false,
|
||||
organizationId,
|
||||
integration.name,
|
||||
undefined,
|
||||
'social',
|
||||
integration.internalId,
|
||||
integration.providerIdentifier,
|
||||
authResult.accessToken,
|
||||
'',
|
||||
authResult.expiresIn,
|
||||
undefined,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
AuthService.signJWT(
|
||||
JSON.parse(Buffer.from(body.cookies, 'base64').toString())
|
||||
)
|
||||
);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
import { Body, Controller, Delete, Get, Post, Put } from '@nestjs/common';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
import { Organization } from '@prisma/client';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { OAuthService } from '@gitroom/nestjs-libraries/database/prisma/oauth/oauth.service';
|
||||
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
|
||||
import {
|
||||
AuthorizationActions,
|
||||
Sections,
|
||||
} from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
import { CreateOAuthAppDto } from '@gitroom/nestjs-libraries/dtos/oauth/create-oauth-app.dto';
|
||||
import { UpdateOAuthAppDto } from '@gitroom/nestjs-libraries/dtos/oauth/update-oauth-app.dto';
|
||||
|
||||
@ApiTags('OAuth App')
|
||||
@Controller('/user/oauth-app')
|
||||
export class OAuthAppController {
|
||||
constructor(private _oauthService: OAuthService) {}
|
||||
|
||||
@Get('/')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
|
||||
async getApp(@GetOrgFromRequest() org: Organization) {
|
||||
return this._oauthService.getApp(org.id);
|
||||
}
|
||||
|
||||
@Post('/')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
|
||||
async createApp(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: CreateOAuthAppDto
|
||||
) {
|
||||
return this._oauthService.createApp(org.id, body);
|
||||
}
|
||||
|
||||
@Put('/')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
|
||||
async updateApp(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: UpdateOAuthAppDto
|
||||
) {
|
||||
return this._oauthService.updateApp(org.id, body);
|
||||
}
|
||||
|
||||
@Delete('/')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
|
||||
async deleteApp(@GetOrgFromRequest() org: Organization) {
|
||||
return this._oauthService.deleteApp(org.id);
|
||||
}
|
||||
|
||||
@Post('/rotate-secret')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
|
||||
async rotateSecret(@GetOrgFromRequest() org: Organization) {
|
||||
return this._oauthService.rotateSecret(org.id);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Post,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { OAuthService } from '@gitroom/nestjs-libraries/database/prisma/oauth/oauth.service';
|
||||
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
import { User, Organization } from '@prisma/client';
|
||||
import { AuthorizeOAuthQueryDto, ApproveOAuthDto } from '@gitroom/nestjs-libraries/dtos/oauth/authorize-oauth.dto';
|
||||
import { TokenExchangeDto } from '@gitroom/nestjs-libraries/dtos/oauth/token-exchange.dto';
|
||||
|
||||
@ApiTags('OAuth')
|
||||
@Controller('/oauth')
|
||||
export class OAuthController {
|
||||
constructor(private _oauthService: OAuthService) {}
|
||||
|
||||
@Get('/authorize')
|
||||
async authorize(@Query() query: AuthorizeOAuthQueryDto) {
|
||||
const app = await this._oauthService.validateAuthorizationRequest(
|
||||
query.client_id
|
||||
);
|
||||
|
||||
return {
|
||||
app: {
|
||||
name: app.name,
|
||||
description: app.description,
|
||||
picture: app.picture,
|
||||
clientId: app.clientId,
|
||||
redirectUrl: app.redirectUrl,
|
||||
},
|
||||
state: query.state,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/token')
|
||||
async token(@Body() body: TokenExchangeDto) {
|
||||
if (body.grant_type !== 'authorization_code') {
|
||||
throw new HttpException(
|
||||
{ error: 'unsupported_grant_type' },
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
return this._oauthService.exchangeCodeForToken(
|
||||
body.code,
|
||||
body.client_id,
|
||||
body.client_secret
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiTags('OAuth')
|
||||
@Controller('/oauth')
|
||||
export class OAuthAuthorizedController {
|
||||
constructor(private _oauthService: OAuthService) {}
|
||||
|
||||
@Post('/authorize')
|
||||
async approveOrDeny(
|
||||
@Body() body: ApproveOAuthDto,
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() org: Organization
|
||||
) {
|
||||
const app = await this._oauthService.validateAuthorizationRequest(
|
||||
body.client_id
|
||||
);
|
||||
|
||||
if (body.action === 'deny') {
|
||||
const redirectUrl = new URL(app.redirectUrl);
|
||||
redirectUrl.searchParams.set('error', 'access_denied');
|
||||
if (body.state) {
|
||||
redirectUrl.searchParams.set('state', body.state);
|
||||
}
|
||||
return { redirect: redirectUrl.toString() };
|
||||
}
|
||||
|
||||
const code = await this._oauthService.createAuthorizationCode(
|
||||
app.id,
|
||||
user.id,
|
||||
org.id
|
||||
);
|
||||
|
||||
const redirectUrl = new URL(app.redirectUrl);
|
||||
redirectUrl.searchParams.set('code', code);
|
||||
if (body.state) {
|
||||
redirectUrl.searchParams.set('state', body.state);
|
||||
}
|
||||
return { redirect: redirectUrl.toString() };
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ import {
|
|||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpException,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
|
|
@ -14,9 +13,10 @@ import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/po
|
|||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
import { Organization, User } from '@prisma/client';
|
||||
import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';
|
||||
import { GetPostsListDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.list.dto';
|
||||
import { StarsService } from '@gitroom/nestjs-libraries/database/prisma/stars/stars.service';
|
||||
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service';
|
||||
import { GeneratorDto } from '@gitroom/nestjs-libraries/dtos/generator/generator.dto';
|
||||
import { CreateGeneratedPostsDto } from '@gitroom/nestjs-libraries/dtos/generator/create.generated.posts.dto';
|
||||
import { AgentGraphService } from '@gitroom/nestjs-libraries/agent/agent.graph.service';
|
||||
|
|
@ -24,16 +24,15 @@ import { Response } from 'express';
|
|||
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
|
||||
import { ShortLinkService } from '@gitroom/nestjs-libraries/short-linking/short.link.service';
|
||||
import { CreateTagDto } from '@gitroom/nestjs-libraries/dtos/posts/create.tag.dto';
|
||||
import {
|
||||
AuthorizationActions,
|
||||
Sections,
|
||||
} from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
|
||||
@ApiTags('Posts')
|
||||
@Controller('/posts')
|
||||
export class PostsController {
|
||||
constructor(
|
||||
private _postsService: PostsService,
|
||||
private _starsService: StarsService,
|
||||
private _messagesService: MessagesService,
|
||||
private _agentGraphService: AgentGraphService,
|
||||
private _shortLinkService: ShortLinkService
|
||||
) {}
|
||||
|
|
@ -46,28 +45,19 @@ export class PostsController {
|
|||
return this._postsService.getStatistics(org.id, id);
|
||||
}
|
||||
|
||||
@Get('/:id/missing')
|
||||
async getMissingContent(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
return this._postsService.getMissingContent(org.id, id);
|
||||
}
|
||||
|
||||
@Put('/:id/release-id')
|
||||
async updateReleaseId(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string,
|
||||
@Body('releaseId') releaseId: string
|
||||
) {
|
||||
return this._postsService.updateReleaseId(org.id, id, releaseId);
|
||||
}
|
||||
|
||||
@Post('/should-shortlink')
|
||||
async shouldShortlink(@Body() body: { messages: string[] }) {
|
||||
return { ask: this._shortLinkService.askShortLinkedin(body.messages) };
|
||||
}
|
||||
|
||||
@Get('/marketplace/:id')
|
||||
async getMarketplacePosts(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
return this._messagesService.getMarketplaceAvailableOffers(org.id, id);
|
||||
}
|
||||
|
||||
@Post('/:id/comments')
|
||||
async createComment(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
|
|
@ -100,20 +90,16 @@ export class PostsController {
|
|||
return this._postsService.editTag(id, org.id, body);
|
||||
}
|
||||
|
||||
@Delete('/tags/:id')
|
||||
async deleteTag(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
return this._postsService.deleteTag(id, org.id);
|
||||
}
|
||||
|
||||
@Get('/')
|
||||
async getPosts(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Query() query: GetPostsDto
|
||||
) {
|
||||
return this._postsService.getPostsMinified(org.id, query);
|
||||
const posts = await this._postsService.getPosts(org.id, query);
|
||||
|
||||
return {
|
||||
posts,
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/find-slot')
|
||||
|
|
@ -129,12 +115,9 @@ export class PostsController {
|
|||
return { date: await this._postsService.findFreeDateTime(org.id, id) };
|
||||
}
|
||||
|
||||
@Get('/list')
|
||||
async getPostsList(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Query() query: GetPostsListDto
|
||||
) {
|
||||
return this._postsService.getPostsList(org.id, query);
|
||||
@Get('/predict-trending')
|
||||
predictTrending() {
|
||||
return this._starsService.predictTrending();
|
||||
}
|
||||
|
||||
@Get('/old')
|
||||
|
|
@ -145,23 +128,6 @@ export class PostsController {
|
|||
return this._postsService.getOldPosts(org.id, date);
|
||||
}
|
||||
|
||||
@Get('/group/:group/debug-export')
|
||||
async getPostGroupDebugExport(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@GetUserFromRequest() user: User,
|
||||
@Param('group') group: string
|
||||
) {
|
||||
if (!user.isSuperAdmin) {
|
||||
throw new HttpException('Forbidden', 403);
|
||||
}
|
||||
return this._postsService.getPostGroupDebugExport(org.id, group);
|
||||
}
|
||||
|
||||
@Get('/group/:group')
|
||||
getPostsByGroup(@GetOrgFromRequest() org: Organization, @Param('group') group: string) {
|
||||
return this._postsService.getPostsByGroup(org.id, group);
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
getPost(@GetOrgFromRequest() org: Organization, @Param('id') id: string) {
|
||||
return this._postsService.getPost(org.id, id);
|
||||
|
|
@ -175,7 +141,7 @@ export class PostsController {
|
|||
) {
|
||||
console.log(JSON.stringify(rawBody, null, 2));
|
||||
const body = await this._postsService.mapTypeToPost(rawBody, org.id);
|
||||
return this._postsService.createPost(org.id, body, 'WEB');
|
||||
return this._postsService.createPost(org.id, body);
|
||||
}
|
||||
|
||||
@Post('/generator/draft')
|
||||
|
|
@ -214,10 +180,9 @@ export class PostsController {
|
|||
changeDate(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string,
|
||||
@Body('date') date: string,
|
||||
@Body('action') action: 'schedule' | 'update' = 'schedule'
|
||||
@Body('date') date: string
|
||||
) {
|
||||
return this._postsService.changeDate(org.id, id, date, action);
|
||||
return this._postsService.changeDate(org.id, id, date);
|
||||
}
|
||||
|
||||
@Post('/separate-posts')
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
StreamableFile,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AgenciesService } from '@gitroom/nestjs-libraries/database/prisma/agencies/agencies.service';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import { TrackService } from '@gitroom/nestjs-libraries/track/track.service';
|
||||
import { RealIP } from 'nestjs-real-ip';
|
||||
|
|
@ -20,14 +21,8 @@ import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
|||
import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management';
|
||||
import { AgentGraphInsertService } from '@gitroom/nestjs-libraries/agent/agent.graph.insert.service';
|
||||
import { Nowpayments } from '@gitroom/nestjs-libraries/crypto/nowpayments';
|
||||
import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
|
||||
import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
||||
import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing';
|
||||
import { Readable, pipeline } from 'stream';
|
||||
import { promisify } from 'util';
|
||||
import { OnlyURL } from '@gitroom/nestjs-libraries/dtos/webhooks/webhooks.dto';
|
||||
import { isSafePublicHttpsUrl } from '@gitroom/nestjs-libraries/dtos/webhooks/webhook.url.validator';
|
||||
import { ssrfSafeDispatcher } from '@gitroom/nestjs-libraries/dtos/webhooks/ssrf.safe.dispatcher';
|
||||
|
||||
const pump = promisify(pipeline);
|
||||
|
||||
|
|
@ -35,11 +30,11 @@ const pump = promisify(pipeline);
|
|||
@Controller('/public')
|
||||
export class PublicController {
|
||||
constructor(
|
||||
private _agenciesService: AgenciesService,
|
||||
private _trackService: TrackService,
|
||||
private _agentGraphInsertService: AgentGraphInsertService,
|
||||
private _postsService: PostsService,
|
||||
private _nowpayments: Nowpayments,
|
||||
private _subscriptionService: SubscriptionService
|
||||
private _nowpayments: Nowpayments
|
||||
) {}
|
||||
@Post('/agent')
|
||||
async createAgent(@Body() body: { text: string; apiKey: string }) {
|
||||
|
|
@ -53,6 +48,26 @@ export class PublicController {
|
|||
return this._agentGraphInsertService.newPost(body.text);
|
||||
}
|
||||
|
||||
@Get('/agencies-list')
|
||||
async getAgencyByUser() {
|
||||
return this._agenciesService.getAllAgencies();
|
||||
}
|
||||
|
||||
@Get('/agencies-list-slug')
|
||||
async getAgencySlug() {
|
||||
return this._agenciesService.getAllAgenciesSlug();
|
||||
}
|
||||
|
||||
@Get('/agencies-information/:agency')
|
||||
async getAgencyInformation(@Param('agency') agency: string) {
|
||||
return this._agenciesService.getAgencyInformation(agency);
|
||||
}
|
||||
|
||||
@Get('/agencies-list-count')
|
||||
async getAgenciesCount() {
|
||||
return this._agenciesService.getCount();
|
||||
}
|
||||
|
||||
@Get(`/posts/:id`)
|
||||
async getPreview(@Param('id') id: string) {
|
||||
return (await this._postsService.getPostsRecursively(id, true)).map(
|
||||
|
|
@ -130,32 +145,6 @@ export class PublicController {
|
|||
});
|
||||
}
|
||||
|
||||
@Post('/modify-subscription')
|
||||
async modifySubscription(@Body('params') params: string) {
|
||||
try {
|
||||
const load = AuthService.verifyJWT(params) as {
|
||||
orgId: string;
|
||||
billing: 'FREE' | 'STANDARD' | 'TEAM' | 'PRO' | 'ULTIMATE';
|
||||
};
|
||||
|
||||
if (!load || !load.orgId || !load.billing || !pricing[load.billing]) {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const totalChannels = pricing[load.billing].channel || 0;
|
||||
|
||||
await this._subscriptionService.modifySubscriptionByOrg(
|
||||
load.orgId,
|
||||
totalChannels,
|
||||
load.billing
|
||||
);
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
|
||||
@Post('/crypto/:path')
|
||||
async cryptoPost(@Body() body: any, @Param('path') path: string) {
|
||||
console.log('cryptoPost', body, path);
|
||||
|
|
@ -164,11 +153,10 @@ export class PublicController {
|
|||
|
||||
@Get('/stream')
|
||||
async streamFile(
|
||||
@Query() query: OnlyURL,
|
||||
@Query('url') url: string,
|
||||
@Res() res: Response,
|
||||
@Req() req: Request
|
||||
) {
|
||||
const { url } = query;
|
||||
if (!url.endsWith('mp4')) {
|
||||
return res.status(400).send('Invalid video URL');
|
||||
}
|
||||
|
|
@ -178,47 +166,7 @@ export class PublicController {
|
|||
req.on('aborted', onClose);
|
||||
res.on('close', onClose);
|
||||
|
||||
// Manually follow redirects so every hop is re-validated against
|
||||
// the SSRF blocklist (see GHSA-34w8-5j2v-h6ww). `fetch` defaults to
|
||||
// `redirect: 'follow'`, which bypasses the DTO-level URL check.
|
||||
const MAX_REDIRECTS = 5;
|
||||
let currentUrl = url;
|
||||
let r: globalThis.Response | undefined;
|
||||
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
||||
if (!(await isSafePublicHttpsUrl(currentUrl))) {
|
||||
return res.status(400).send('Blocked URL');
|
||||
}
|
||||
|
||||
r = await fetch(currentUrl, {
|
||||
signal: ac.signal,
|
||||
redirect: 'manual',
|
||||
// @ts-ignore — undici option, not in lib.dom fetch types
|
||||
dispatcher: ssrfSafeDispatcher,
|
||||
});
|
||||
|
||||
if (r.status >= 300 && r.status < 400) {
|
||||
const location = r.headers.get('location');
|
||||
if (!location) {
|
||||
return res.status(502).send('Redirect without Location');
|
||||
}
|
||||
try {
|
||||
currentUrl = new URL(location, currentUrl).toString();
|
||||
} catch {
|
||||
return res.status(400).send('Invalid redirect target');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (!r) {
|
||||
return res.status(502).send('No upstream response');
|
||||
}
|
||||
|
||||
if (r.status >= 300 && r.status < 400) {
|
||||
return res.status(508).send('Too many redirects');
|
||||
}
|
||||
const r = await fetch(url, { signal: ac.signal });
|
||||
|
||||
if (!r.ok && r.status !== 206) {
|
||||
res.status(r.status);
|
||||
|
|
@ -241,6 +189,7 @@ export class PublicController {
|
|||
|
||||
try {
|
||||
await pump(Readable.fromWeb(r.body as any), res);
|
||||
} catch (err) {}
|
||||
} catch (err) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
import { Organization } from '@prisma/client';
|
||||
import { StarsService } from '@gitroom/nestjs-libraries/database/prisma/stars/stars.service';
|
||||
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
|
||||
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
|
||||
import { AddTeamMemberDto } from '@gitroom/nestjs-libraries/dtos/settings/add.team.member.dto';
|
||||
import { ShortlinkPreferenceDto } from '@gitroom/nestjs-libraries/dtos/settings/shortlink-preference.dto';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
|
||||
|
|
@ -12,9 +12,95 @@ import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/p
|
|||
@Controller('/settings')
|
||||
export class SettingsController {
|
||||
constructor(
|
||||
private _starsService: StarsService,
|
||||
private _organizationService: OrganizationService
|
||||
) {}
|
||||
|
||||
@Get('/github')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
|
||||
async getConnectedGithubAccounts(@GetOrgFromRequest() org: Organization) {
|
||||
return {
|
||||
github: (
|
||||
await this._starsService.getGitHubRepositoriesByOrgId(org.id)
|
||||
).map((repo) => ({
|
||||
id: repo.id,
|
||||
login: repo.login,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/github')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
|
||||
async addGitHub(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body('code') code: string
|
||||
) {
|
||||
if (!code) {
|
||||
throw new Error('No code provided');
|
||||
}
|
||||
await this._starsService.addGitHub(org.id, code);
|
||||
}
|
||||
|
||||
@Get('/github/url')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
|
||||
authUrl() {
|
||||
return {
|
||||
url: `https://github.com/login/oauth/authorize?client_id=${
|
||||
process.env.GITHUB_CLIENT_ID
|
||||
}&scope=${encodeURIComponent(
|
||||
'user:email'
|
||||
)}&redirect_uri=${encodeURIComponent(
|
||||
`${process.env.FRONTEND_URL}/settings`
|
||||
)}`,
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/organizations/:id')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
|
||||
async getOrganizations(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
return {
|
||||
organizations: await this._starsService.getOrganizations(org.id, id),
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/organizations/:id/:github')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
|
||||
async getRepositories(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string,
|
||||
@Param('github') github: string
|
||||
) {
|
||||
return {
|
||||
repositories: await this._starsService.getRepositoriesOfOrganization(
|
||||
org.id,
|
||||
id,
|
||||
github
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/organizations/:id')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
|
||||
async updateGitHubLogin(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string,
|
||||
@Body('login') login: string
|
||||
) {
|
||||
return this._starsService.updateGitHubLogin(org.id, id, login);
|
||||
}
|
||||
|
||||
@Delete('/repository/:id')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
|
||||
async deleteRepository(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
return this._starsService.deleteRepository(org.id, id);
|
||||
}
|
||||
|
||||
@Get('/team')
|
||||
@CheckPolicies(
|
||||
[AuthorizationActions.Create, Sections.TEAM_MEMBERS],
|
||||
|
|
@ -47,21 +133,4 @@ export class SettingsController {
|
|||
) {
|
||||
return this._organizationService.deleteTeamMember(org, id);
|
||||
}
|
||||
|
||||
@Get('/shortlink')
|
||||
async getShortlinkPreference(@GetOrgFromRequest() org: Organization) {
|
||||
return this._organizationService.getShortlinkPreference(org.id);
|
||||
}
|
||||
|
||||
@Post('/shortlink')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
|
||||
async updateShortlinkPreference(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: ShortlinkPreferenceDto
|
||||
) {
|
||||
return this._organizationService.updateShortlinkPreference(
|
||||
org.id,
|
||||
body.shortlink
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,47 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Header,
|
||||
HttpException,
|
||||
Param,
|
||||
Post,
|
||||
RawBodyRequest,
|
||||
Req,
|
||||
} from '@nestjs/common';
|
||||
import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { CodesService } from '@gitroom/nestjs-libraries/services/codes.service';
|
||||
|
||||
@ApiTags('Stripe')
|
||||
@Controller('/stripe')
|
||||
export class StripeController {
|
||||
constructor(
|
||||
private readonly _stripeService: StripeService,
|
||||
private readonly _codesService: CodesService
|
||||
) {}
|
||||
@Post('/connect')
|
||||
stripeConnect(@Req() req: RawBodyRequest<Request>) {
|
||||
const event = this._stripeService.validateRequest(
|
||||
req.rawBody,
|
||||
// @ts-ignore
|
||||
req.headers['stripe-signature'],
|
||||
process.env.STRIPE_SIGNING_KEY_CONNECT
|
||||
);
|
||||
|
||||
// Maybe it comes from another stripe webhook
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
if (event?.data?.object?.metadata?.service !== 'gitroom') {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case 'account.updated':
|
||||
return this._stripeService.updateAccount(event);
|
||||
default:
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
|
||||
@Post('/')
|
||||
stripe(@Req() req: RawBodyRequest<Request>) {
|
||||
|
|
@ -38,6 +66,8 @@ export class StripeController {
|
|||
switch (event.type) {
|
||||
case 'invoice.payment_succeeded':
|
||||
return this._stripeService.paymentSucceeded(event);
|
||||
case 'account.updated':
|
||||
return this._stripeService.updateAccount(event);
|
||||
case 'customer.subscription.created':
|
||||
return this._stripeService.createSubscription(event);
|
||||
case 'customer.subscription.updated':
|
||||
|
|
@ -51,4 +81,11 @@ export class StripeController {
|
|||
throw new HttpException(e, 500);
|
||||
}
|
||||
}
|
||||
|
||||
@Get('/lifetime-deal-codes/:provider')
|
||||
@Header('Content-disposition', 'attachment; filename=codes.csv')
|
||||
@Header('Content-type', 'text/csv')
|
||||
async getStripeCodes(@Param('provider') providerToken: string) {
|
||||
return this._codesService.generateCodes(providerToken);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import { Organization } from '@prisma/client';
|
|||
import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
||||
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
|
||||
import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service';
|
||||
import { ImportMediaDto } from '@gitroom/nestjs-libraries/dtos/third-party/import-media.dto';
|
||||
|
||||
@ApiTags('Third Party')
|
||||
@Controller('/third-party')
|
||||
|
|
@ -122,52 +121,6 @@ export class ThirdPartyController {
|
|||
);
|
||||
}
|
||||
|
||||
@Post('/:id/import')
|
||||
async importMedia(
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Param('id') id: string,
|
||||
@Body() body: ImportMediaDto
|
||||
) {
|
||||
const thirdParty = await this._thirdPartyManager.getIntegrationById(
|
||||
organization.id,
|
||||
id
|
||||
);
|
||||
|
||||
if (!thirdParty) {
|
||||
throw new HttpException('Integration not found', 404);
|
||||
}
|
||||
|
||||
const thirdPartyInstance = this._thirdPartyManager.getThirdPartyByName(
|
||||
thirdParty.identifier
|
||||
);
|
||||
|
||||
if (!thirdPartyInstance) {
|
||||
throw new HttpException('Invalid identifier', 400);
|
||||
}
|
||||
|
||||
const downloadUrls = await thirdPartyInstance?.instance?.['importMedia']?.(
|
||||
AuthService.fixedDecryption(thirdParty.apiKey),
|
||||
body.items
|
||||
);
|
||||
|
||||
if (!downloadUrls || !Array.isArray(downloadUrls)) {
|
||||
throw new HttpException('Import not supported', 400);
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (const item of downloadUrls) {
|
||||
const file = await this.storage.uploadSimple(item.url);
|
||||
const saved = await this._mediaService.saveFile(
|
||||
organization.id,
|
||||
item.name || file.split('/').pop(),
|
||||
file
|
||||
);
|
||||
results.push(saved);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@Post('/:identifier')
|
||||
async addApiKey(
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
Res,
|
||||
} from '@nestjs/common';
|
||||
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
|
||||
import { sign } from 'jsonwebtoken';
|
||||
import { Organization, User } from '@prisma/client';
|
||||
import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
|
|
@ -23,7 +22,6 @@ import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions
|
|||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service';
|
||||
import { UserDetailDto } from '@gitroom/nestjs-libraries/dtos/users/user.details.dto';
|
||||
import { EmailNotificationsDto } from '@gitroom/nestjs-libraries/dtos/users/email-notifications.dto';
|
||||
import { HttpForbiddenException } from '@gitroom/nestjs-libraries/services/exception.filter';
|
||||
import { RealIP } from 'nestjs-real-ip';
|
||||
import { UserAgent } from '@gitroom/nestjs-libraries/user/user.agent';
|
||||
|
|
@ -43,23 +41,6 @@ export class UsersController {
|
|||
private _userService: UsersService,
|
||||
private _trackService: TrackService
|
||||
) {}
|
||||
@Get('/agent-media-sso')
|
||||
async getAgentMediaSsoUrl(
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() organization: Organization
|
||||
) {
|
||||
if (!process.env.AGENT_MEDIA_SSO_KEY) {
|
||||
throw new HttpException('Agent Media SSO is not configured', 400);
|
||||
}
|
||||
|
||||
const token = sign(
|
||||
{ id: organization.id, displayName: organization.name },
|
||||
process.env.AGENT_MEDIA_SSO_KEY
|
||||
);
|
||||
|
||||
return { url: `https://agent-media.ai/sso/${token}` };
|
||||
}
|
||||
|
||||
@Get('/self')
|
||||
async getSelf(
|
||||
@GetUserFromRequest() user: User,
|
||||
|
|
@ -87,14 +68,13 @@ export class UsersController {
|
|||
impersonate: !!impersonate,
|
||||
isTrailing: !process.env.STRIPE_PUBLISHABLE_KEY ? false : organization?.isTrailing,
|
||||
allowTrial: organization?.allowTrial,
|
||||
streakSince: organization?.streakSince || null,
|
||||
// @ts-ignore
|
||||
publicApi: organization?.users[0]?.role === 'SUPERADMIN' || organization?.users[0]?.role === 'ADMIN' ? organization?.apiKey : '',
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/personal')
|
||||
async getPersonalInformation(@GetUserFromRequest() user: User) {
|
||||
async getPersonal(@GetUserFromRequest() user: User) {
|
||||
return this._userService.getPersonal(user.id);
|
||||
}
|
||||
|
||||
|
|
@ -145,25 +125,6 @@ export class UsersController {
|
|||
return this._userService.changePersonal(user.id, body);
|
||||
}
|
||||
|
||||
@Get('/email-notifications')
|
||||
async getEmailNotifications(@GetUserFromRequest() user: User) {
|
||||
return this._userService.getEmailNotifications(user.id);
|
||||
}
|
||||
|
||||
@Post('/email-notifications')
|
||||
async updateEmailNotifications(
|
||||
@GetUserFromRequest() user: User,
|
||||
@Body() body: EmailNotificationsDto
|
||||
) {
|
||||
return this._userService.updateEmailNotifications(user.id, body);
|
||||
}
|
||||
|
||||
@Post('/api-key/rotate')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
|
||||
async rotateApiKey(@GetOrgFromRequest() organization: Organization) {
|
||||
return this._orgService.updateApiKey(organization.id);
|
||||
}
|
||||
|
||||
@Get('/subscription')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
|
||||
async getSubscription(@GetOrgFromRequest() organization: Organization) {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ import { ApiTags } from '@nestjs/swagger';
|
|||
import { WebhooksService } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.service';
|
||||
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
|
||||
import {
|
||||
OnlyURL, UpdateDto, WebhooksDto
|
||||
UpdateDto,
|
||||
WebhooksDto,
|
||||
} from '@gitroom/nestjs-libraries/dtos/webhooks/webhooks.dto';
|
||||
import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
|
||||
|
|
@ -54,9 +55,9 @@ export class WebhookController {
|
|||
}
|
||||
|
||||
@Post('/send')
|
||||
async sendWebhook(@Body() body: any, @Query() query: OnlyURL) {
|
||||
async sendWebhook(@Body() body: any, @Query('url') url: string) {
|
||||
try {
|
||||
await fetch(query.url, {
|
||||
await fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { DatabaseModule } from '@gitroom/nestjs-libraries/database/prisma/databa
|
|||
import { ApiModule } from '@gitroom/backend/api/api.module';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { PoliciesGuard } from '@gitroom/backend/services/auth/permissions/permissions.guard';
|
||||
import { BullMqModule } from '@gitroom/nestjs-libraries/bull-mq-transport-new/bull.mq.module';
|
||||
import { PublicApiModule } from '@gitroom/backend/public-api/public.api.module';
|
||||
import { ThrottlerBehindProxyGuard } from '@gitroom/nestjs-libraries/throttler/throttler.provider';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
|
|
@ -12,16 +13,12 @@ import { VideoModule } from '@gitroom/nestjs-libraries/videos/video.module';
|
|||
import { SentryModule } from '@sentry/nestjs/setup';
|
||||
import { FILTER } from '@gitroom/nestjs-libraries/sentry/sentry.exception';
|
||||
import { ChatModule } from '@gitroom/nestjs-libraries/chat/chat.module';
|
||||
import { getTemporalModule } from '@gitroom/nestjs-libraries/temporal/temporal.module';
|
||||
import { TemporalRegisterMissingSearchAttributesModule } from '@gitroom/nestjs-libraries/temporal/temporal.register';
|
||||
import { InfiniteWorkflowRegisterModule } from '@gitroom/nestjs-libraries/temporal/infinite.workflow.register';
|
||||
import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis';
|
||||
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
SentryModule.forRoot(),
|
||||
BullMqModule,
|
||||
DatabaseModule,
|
||||
ApiModule,
|
||||
PublicApiModule,
|
||||
|
|
@ -29,18 +26,12 @@ import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
|
|||
ThirdPartyModule,
|
||||
VideoModule,
|
||||
ChatModule,
|
||||
getTemporalModule(false),
|
||||
TemporalRegisterMissingSearchAttributesModule,
|
||||
InfiniteWorkflowRegisterModule,
|
||||
ThrottlerModule.forRoot({
|
||||
throttlers: [
|
||||
{
|
||||
ttl: 3600000,
|
||||
limit: process.env.API_LIMIT ? Number(process.env.API_LIMIT) : 90,
|
||||
},
|
||||
],
|
||||
storage: new ThrottlerStorageRedisService(ioRedis),
|
||||
}),
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: 3600000,
|
||||
limit: process.env.API_LIMIT ? Number(process.env.API_LIMIT) : 30,
|
||||
},
|
||||
]),
|
||||
],
|
||||
controllers: [],
|
||||
providers: [
|
||||
|
|
@ -55,6 +46,7 @@ import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
|
|||
},
|
||||
],
|
||||
exports: [
|
||||
BullMqModule,
|
||||
DatabaseModule,
|
||||
ApiModule,
|
||||
PublicApiModule,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
import { initializeSentry } from '@gitroom/nestjs-libraries/sentry/initialize.sentry';
|
||||
initializeSentry('backend', true);
|
||||
import compression from 'compression';
|
||||
|
||||
import { loadSwagger } from '@gitroom/helpers/swagger/load.swagger';
|
||||
import { json } from 'express';
|
||||
import { Runtime } from '@temporalio/worker';
|
||||
Runtime.install({ shutdownSignals: [] });
|
||||
|
||||
process.env.TZ = 'UTC';
|
||||
|
||||
|
|
@ -24,14 +21,7 @@ async function start() {
|
|||
rawBody: true,
|
||||
cors: {
|
||||
...(!process.env.NOT_SECURED ? { credentials: true } : {}),
|
||||
allowedHeaders: [
|
||||
'Content-Type',
|
||||
'Authorization',
|
||||
'auth',
|
||||
'showorg',
|
||||
'impersonate',
|
||||
'x-copilotkit-runtime-client-gql-version',
|
||||
],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'x-copilotkit-runtime-client-gql-version'],
|
||||
exposedHeaders: [
|
||||
'reload',
|
||||
'onboarding',
|
||||
|
|
@ -55,12 +45,11 @@ async function start() {
|
|||
})
|
||||
);
|
||||
|
||||
app.use(['/copilot/{*splat}', '/posts'], (req: any, res: any, next: any) => {
|
||||
app.use('/copilot/*', (req: any, res: any, next: any) => {
|
||||
json({ limit: '50mb' })(req, res, next);
|
||||
});
|
||||
|
||||
app.use(cookieParser());
|
||||
app.use(compression());
|
||||
app.useGlobalFilters(new SubscriptionExceptionFilter());
|
||||
app.useGlobalFilters(new HttpExceptionFilter());
|
||||
|
||||
|
|
@ -70,7 +59,6 @@ async function start() {
|
|||
|
||||
try {
|
||||
await app.listen(port);
|
||||
console.log('Backend started successfully on port ' + port);
|
||||
|
||||
checkConfiguration(); // Do this last, so that users will see obvious issues at the end of the startup log without having to scroll up.
|
||||
|
||||
|
|
|
|||
|
|
@ -6,13 +6,10 @@ import {
|
|||
HttpException,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UploadedFile,
|
||||
UseInterceptors,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { CustomFileValidationPipe } from '@gitroom/nestjs-libraries/upload/custom.upload.validation';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
import { Organization } from '@prisma/client';
|
||||
|
|
@ -23,7 +20,6 @@ import { FileInterceptor } from '@nestjs/platform-express';
|
|||
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
|
||||
import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service';
|
||||
import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';
|
||||
import { ChangePostStatusDto } from '@gitroom/nestjs-libraries/dtos/posts/change.post.status.dto';
|
||||
import {
|
||||
AuthorizationActions,
|
||||
Sections,
|
||||
|
|
@ -31,56 +27,29 @@ import {
|
|||
import { VideoDto } from '@gitroom/nestjs-libraries/dtos/videos/video.dto';
|
||||
import { VideoFunctionDto } from '@gitroom/nestjs-libraries/dtos/videos/video.function.dto';
|
||||
import { UploadDto } from '@gitroom/nestjs-libraries/dtos/media/upload.dto';
|
||||
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
|
||||
import { GetNotificationsDto } from '@gitroom/nestjs-libraries/dtos/notifications/get.notifications.dto';
|
||||
import axios from 'axios';
|
||||
import { Readable } from 'stream';
|
||||
import { ssrfSafeDispatcher } from '@gitroom/nestjs-libraries/dtos/webhooks/ssrf.safe.dispatcher';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { fromBuffer } = require('file-type');
|
||||
|
||||
const PUBLIC_API_ALLOWED_MIME = new Set<string>([
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
'image/avif',
|
||||
'image/bmp',
|
||||
'image/tiff',
|
||||
'video/mp4',
|
||||
]);
|
||||
import { lookup } from 'mime-types';
|
||||
import * as Sentry from '@sentry/nestjs';
|
||||
import {
|
||||
socialIntegrationList,
|
||||
IntegrationManager,
|
||||
} from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
import { getValidationSchemas } from '@gitroom/nestjs-libraries/chat/validation.schemas.helper';
|
||||
import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service';
|
||||
import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
|
||||
|
||||
@ApiTags('Public API')
|
||||
@Controller('/public/v1')
|
||||
export class PublicIntegrationsController {
|
||||
private storage = UploadFactory.createStorage();
|
||||
|
||||
|
||||
constructor(
|
||||
private _integrationService: IntegrationService,
|
||||
private _postsService: PostsService,
|
||||
private _mediaService: MediaService,
|
||||
private _notificationService: NotificationService,
|
||||
private _integrationManager: IntegrationManager,
|
||||
private _refreshIntegrationService: RefreshIntegrationService
|
||||
private _mediaService: MediaService
|
||||
) {}
|
||||
|
||||
@Post('/upload')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
@UsePipes(new CustomFileValidationPipe())
|
||||
async uploadSimple(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@UploadedFile('file') file: Express.Multer.File
|
||||
) {
|
||||
Sentry.metrics.count('public_api-request', 1);
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
if (!file) {
|
||||
throw new HttpException({ msg: 'No file provided' }, 400);
|
||||
}
|
||||
|
|
@ -98,32 +67,23 @@ export class PublicIntegrationsController {
|
|||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: UploadDto
|
||||
) {
|
||||
Sentry.metrics.count('public_api-request', 1);
|
||||
const response = await fetch(body.url, {
|
||||
// @ts-ignore — undici option, not in lib.dom fetch types
|
||||
dispatcher: ssrfSafeDispatcher,
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
const response = await axios.get(body.url, {
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new HttpException({ msg: 'Failed to fetch URL' }, 400);
|
||||
}
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
const detected = await fromBuffer(buffer);
|
||||
if (!detected || !PUBLIC_API_ALLOWED_MIME.has(detected.mime)) {
|
||||
throw new HttpException({ msg: 'Unsupported file type.' }, 400);
|
||||
}
|
||||
const mimetype = detected.mime;
|
||||
const ext = detected.ext;
|
||||
|
||||
const buffer = Buffer.from(response.data);
|
||||
|
||||
const getFile = await this.storage.uploadFile({
|
||||
buffer,
|
||||
mimetype,
|
||||
mimetype: lookup(body?.url?.split?.('?')?.[0]) || 'image/jpeg',
|
||||
size: buffer.length,
|
||||
path: '',
|
||||
fieldname: '',
|
||||
destination: '',
|
||||
stream: new Readable(),
|
||||
filename: '',
|
||||
originalname: `upload.${ext}`,
|
||||
originalname: '',
|
||||
encoding: '',
|
||||
});
|
||||
|
||||
|
|
@ -139,7 +99,7 @@ export class PublicIntegrationsController {
|
|||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id?: string
|
||||
) {
|
||||
Sentry.metrics.count('public_api-request', 1);
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
return { date: await this._postsService.findFreeDateTime(org.id, id) };
|
||||
}
|
||||
|
||||
|
|
@ -148,7 +108,7 @@ export class PublicIntegrationsController {
|
|||
@GetOrgFromRequest() org: Organization,
|
||||
@Query() query: GetPostsDto
|
||||
) {
|
||||
Sentry.metrics.count('public_api-request', 1);
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
const posts = await this._postsService.getPosts(org.id, query);
|
||||
return {
|
||||
posts,
|
||||
|
|
@ -162,7 +122,7 @@ export class PublicIntegrationsController {
|
|||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() rawBody: any
|
||||
) {
|
||||
Sentry.metrics.count('public_api-request', 1);
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
const body = await this._postsService.mapTypeToPost(
|
||||
rawBody,
|
||||
org.id,
|
||||
|
|
@ -170,63 +130,29 @@ export class PublicIntegrationsController {
|
|||
);
|
||||
body.type = rawBody.type;
|
||||
|
||||
if (
|
||||
process.env.RESTRICT_UPLOAD_DOMAINS &&
|
||||
body.posts.some((p) =>
|
||||
p.value.some((a) =>
|
||||
a.image.some(
|
||||
(i) => i.path.indexOf(process.env.RESTRICT_UPLOAD_DOMAINS) === -1
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
throw new HttpException(
|
||||
{
|
||||
msg: `All media must be uploaded through our upload API route and contain the domain: ${process.env.RESTRICT_UPLOAD_DOMAINS}`,
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
const allowedCreationMethods = ['CLI', 'API'] as const;
|
||||
const creationMethod = allowedCreationMethods.includes(
|
||||
rawBody.creationMethod
|
||||
)
|
||||
? (rawBody.creationMethod as 'CLI' | 'API')
|
||||
: 'API';
|
||||
|
||||
console.log(JSON.stringify(body, null, 2));
|
||||
return this._postsService.createPost(org.id, body, creationMethod);
|
||||
return this._postsService.createPost(org.id, body);
|
||||
}
|
||||
|
||||
@Delete('/posts/:id')
|
||||
async deletePost(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string
|
||||
@Param() body: { id: string }
|
||||
) {
|
||||
Sentry.metrics.count('public_api-request', 1);
|
||||
const getPostById = await this._postsService.getPost(org.id, id);
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
const getPostById = await this._postsService.getPost(org.id, body.id);
|
||||
return this._postsService.deletePost(org.id, getPostById.group);
|
||||
}
|
||||
|
||||
@Delete('/posts/group/:group')
|
||||
deletePostByGroup(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('group') group: string
|
||||
) {
|
||||
Sentry.metrics.count('public_api-request', 1);
|
||||
return this._postsService.deletePost(org.id, group);
|
||||
}
|
||||
|
||||
@Get('/is-connected')
|
||||
async getActiveIntegrations(@GetOrgFromRequest() org: Organization) {
|
||||
Sentry.metrics.count('public_api-request', 1);
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
return { connected: true };
|
||||
}
|
||||
|
||||
@Get('/integrations')
|
||||
async listIntegration(@GetOrgFromRequest() org: Organization) {
|
||||
Sentry.metrics.count('public_api-request', 1);
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
return (await this._integrationService.getIntegrationsList(org.id)).map(
|
||||
(org) => ({
|
||||
id: org.id,
|
||||
|
|
@ -245,271 +171,22 @@ export class PublicIntegrationsController {
|
|||
);
|
||||
}
|
||||
|
||||
@Get('/social/:integration')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.CHANNEL])
|
||||
async getIntegrationUrl(
|
||||
@Param('integration') integration: string,
|
||||
@Query('refresh') refresh: string,
|
||||
@GetOrgFromRequest() org: Organization
|
||||
) {
|
||||
Sentry.metrics.count('public_api-request', 1);
|
||||
if (
|
||||
!this._integrationManager
|
||||
.getAllowedSocialsIntegrations()
|
||||
.includes(integration)
|
||||
) {
|
||||
throw new HttpException({ msg: 'Integration not allowed' }, 400);
|
||||
}
|
||||
|
||||
const integrationProvider =
|
||||
this._integrationManager.getSocialIntegration(integration);
|
||||
|
||||
if (integrationProvider.externalUrl) {
|
||||
throw new HttpException(
|
||||
{
|
||||
msg: 'This integration requires an external URL and is not supported via the public API',
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const { codeVerifier, state, url } =
|
||||
await integrationProvider.generateAuthUrl();
|
||||
|
||||
if (refresh) {
|
||||
await ioRedis.set(`refresh:${state}`, refresh, 'EX', 3600);
|
||||
}
|
||||
|
||||
await ioRedis.set(`organization:${state}`, org.id, 'EX', 3600);
|
||||
await ioRedis.set(`login:${state}`, codeVerifier, 'EX', 3600);
|
||||
|
||||
return { url };
|
||||
} catch (err) {
|
||||
throw new HttpException({ msg: 'Failed to generate auth URL' }, 500);
|
||||
}
|
||||
}
|
||||
|
||||
@Get('/notifications')
|
||||
async getNotifications(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Query() query: GetNotificationsDto
|
||||
) {
|
||||
Sentry.metrics.count('public_api-request', 1);
|
||||
return this._notificationService.getNotificationsPaginated(
|
||||
org.id,
|
||||
query.page ?? 0
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/generate-video')
|
||||
generateVideo(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: VideoDto
|
||||
) {
|
||||
Sentry.metrics.count('public_api-request', 1);
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
return this._mediaService.generateVideo(org, body);
|
||||
}
|
||||
|
||||
@Post('/video/function')
|
||||
videoFunction(@Body() body: VideoFunctionDto) {
|
||||
Sentry.metrics.count('public_api-request', 1);
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
return this._mediaService.videoFunction(
|
||||
body.identifier,
|
||||
body.functionName,
|
||||
body.params
|
||||
);
|
||||
}
|
||||
|
||||
@Delete('/integrations/:id')
|
||||
async deleteChannel(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
Sentry.metrics.count('public_api-request', 1);
|
||||
const isTherePosts = await this._integrationService.getPostsForChannel(
|
||||
org.id,
|
||||
id
|
||||
);
|
||||
if (isTherePosts.length) {
|
||||
for (const post of isTherePosts) {
|
||||
this._postsService.deletePost(org.id, post.group).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
return this._integrationService.deleteChannel(org.id, id);
|
||||
}
|
||||
|
||||
@Get('/integration-settings/:id')
|
||||
async getIntegrationSettings(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
Sentry.metrics.count('public_api-request', 1);
|
||||
const loadIntegration = await this._integrationService.getIntegrationById(
|
||||
org.id,
|
||||
id
|
||||
);
|
||||
|
||||
const verified =
|
||||
JSON.parse(loadIntegration.additionalSettings || '[]')?.find(
|
||||
(p: any) => p?.title === 'Verified'
|
||||
)?.value || false;
|
||||
|
||||
const integration = socialIntegrationList.find(
|
||||
(p) => p.identifier === loadIntegration.providerIdentifier
|
||||
)!;
|
||||
|
||||
if (!integration) {
|
||||
return {
|
||||
output: { rules: '', maxLength: 0, settings: {}, tools: [] as any[] },
|
||||
};
|
||||
}
|
||||
|
||||
const maxLength = integration.maxLength(verified);
|
||||
const schemas = !integration.dto
|
||||
? false
|
||||
: getValidationSchemas()[integration.dto.name];
|
||||
const tools = this._integrationManager.getAllTools();
|
||||
const rules = this._integrationManager.getAllRulesDescription();
|
||||
|
||||
return {
|
||||
output: {
|
||||
rules: rules[integration.identifier],
|
||||
maxLength,
|
||||
settings: !schemas ? 'No additional settings required' : schemas,
|
||||
tools: tools[integration.identifier],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/posts/:id/missing')
|
||||
async getMissingContent(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
Sentry.metrics.count('public_api-request', 1);
|
||||
return this._postsService.getMissingContent(org.id, id);
|
||||
}
|
||||
|
||||
@Put('/posts/:id/status')
|
||||
async changePostStatus(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string,
|
||||
@Body() body: ChangePostStatusDto
|
||||
) {
|
||||
Sentry.metrics.count('public_api-request', 1);
|
||||
return this._postsService.changePostStatus(org.id, id, body.status);
|
||||
}
|
||||
|
||||
@Put('/posts/:id/release-id')
|
||||
async updateReleaseId(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string,
|
||||
@Body('releaseId') releaseId: string
|
||||
) {
|
||||
Sentry.metrics.count('public_api-request', 1);
|
||||
return this._postsService.updateReleaseId(org.id, id, releaseId);
|
||||
}
|
||||
|
||||
@Get('/analytics/:integration')
|
||||
async getAnalytics(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('integration') integration: string,
|
||||
@Query('date') date: string
|
||||
) {
|
||||
Sentry.metrics.count('public_api-request', 1);
|
||||
return this._integrationService.checkAnalytics(org, integration, date);
|
||||
}
|
||||
|
||||
@Get('/analytics/post/:postId')
|
||||
async getPostAnalytics(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('postId') postId: string,
|
||||
@Query('date') date: string
|
||||
) {
|
||||
Sentry.metrics.count('public_api-request', 1);
|
||||
return this._postsService.checkPostAnalytics(org.id, postId, +date);
|
||||
}
|
||||
|
||||
@Post('/integration-trigger/:id')
|
||||
async triggerIntegrationTool(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string,
|
||||
@Body() body: { methodName: string; data: Record<string, string> }
|
||||
) {
|
||||
Sentry.metrics.count('public_api-request', 1);
|
||||
const getIntegration = await this._integrationService.getIntegrationById(
|
||||
org.id,
|
||||
id
|
||||
);
|
||||
|
||||
if (!getIntegration) {
|
||||
throw new HttpException({ msg: 'Integration not found' }, 404);
|
||||
}
|
||||
|
||||
const integrationProvider = socialIntegrationList.find(
|
||||
(p) => p.identifier === getIntegration.providerIdentifier
|
||||
)!;
|
||||
|
||||
if (!integrationProvider) {
|
||||
throw new HttpException({ msg: 'Integration provider not found' }, 404);
|
||||
}
|
||||
|
||||
const tools = this._integrationManager.getAllTools();
|
||||
if (
|
||||
// @ts-ignore
|
||||
!tools[integrationProvider.identifier]?.some(
|
||||
(p: any) => p.methodName === body.methodName
|
||||
) ||
|
||||
// @ts-ignore
|
||||
!integrationProvider[body.methodName]
|
||||
) {
|
||||
throw new HttpException({ msg: 'Tool not found' }, 404);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
const result = await integrationProvider[body.methodName](
|
||||
getIntegration.token,
|
||||
body.data || {},
|
||||
getIntegration.internalId,
|
||||
getIntegration
|
||||
);
|
||||
|
||||
return { output: result };
|
||||
} catch (err) {
|
||||
if (err instanceof RefreshToken) {
|
||||
const data = await this._refreshIntegrationService.refresh(
|
||||
getIntegration
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
await this._integrationService.disconnectChannel(
|
||||
org.id,
|
||||
getIntegration
|
||||
);
|
||||
throw new HttpException(
|
||||
{ msg: 'Channel disconnected due to expired token' },
|
||||
401
|
||||
);
|
||||
}
|
||||
|
||||
const { accessToken } = data;
|
||||
|
||||
if (accessToken) {
|
||||
getIntegration.token = accessToken;
|
||||
|
||||
if (integrationProvider.refreshWait) {
|
||||
await timer(10000);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
throw new HttpException({ msg: 'Unexpected error' }, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { LoginUserDto } from '@gitroom/nestjs-libraries/dtos/auth/login.user.dto
|
|||
import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service';
|
||||
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
|
||||
import { AuthService as AuthChecker } from '@gitroom/helpers/auth/auth.service';
|
||||
import { AuthProviderManager } from '@gitroom/backend/services/auth/providers/providers.manager';
|
||||
import { ProvidersFactory } from '@gitroom/backend/services/auth/providers/providers.factory';
|
||||
import dayjs from 'dayjs';
|
||||
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
|
||||
import { ForgotReturnPasswordDto } from '@gitroom/nestjs-libraries/dtos/auth/forgot-return.password.dto';
|
||||
|
|
@ -18,14 +18,10 @@ export class AuthService {
|
|||
private _userService: UsersService,
|
||||
private _organizationService: OrganizationService,
|
||||
private _notificationService: NotificationService,
|
||||
private _emailService: EmailService,
|
||||
private _providerManager: AuthProviderManager
|
||||
private _emailService: EmailService
|
||||
) {}
|
||||
async canRegister(provider: string) {
|
||||
if (
|
||||
process.env.DISABLE_REGISTRATION !== 'true' ||
|
||||
provider === Provider.GENERIC
|
||||
) {
|
||||
if (process.env.DISABLE_REGISTRATION !== 'true' || provider === Provider.GENERIC) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -43,9 +39,6 @@ export class AuthService {
|
|||
if (process.env.DISALLOW_PLUS && body.email.includes('+')) {
|
||||
throw new Error('Email with plus sign is not allowed');
|
||||
}
|
||||
if (body instanceof CreateOrgUserDto) {
|
||||
body.email = body.email.toLowerCase();
|
||||
}
|
||||
const user = await this._userService.getUserByEmail(body.email);
|
||||
if (body instanceof CreateOrgUserDto) {
|
||||
if (user) {
|
||||
|
|
@ -76,8 +69,7 @@ export class AuthService {
|
|||
await this._emailService.sendEmail(
|
||||
body.email,
|
||||
'Activate your account',
|
||||
`Click <a href="${process.env.FRONTEND_URL}/auth/activate/${obj.jwt}">here</a> to activate your account`,
|
||||
'top'
|
||||
`Click <a href="${process.env.FRONTEND_URL}/auth/activate/${obj.jwt}">here</a> to activate your account`
|
||||
);
|
||||
return obj;
|
||||
}
|
||||
|
|
@ -140,7 +132,7 @@ export class AuthService {
|
|||
ip: string,
|
||||
userAgent: string
|
||||
) {
|
||||
const providerInstance = this._providerManager.getProvider(provider);
|
||||
const providerInstance = ProvidersFactory.loadProvider(provider);
|
||||
const providerUser = await providerInstance.getUser(body.providerToken);
|
||||
|
||||
if (!providerUser) {
|
||||
|
|
@ -166,54 +158,16 @@ export class AuthService {
|
|||
password: '',
|
||||
provider,
|
||||
providerId: providerUser.id,
|
||||
datafast_visitor_id: body.datafast_visitor_id,
|
||||
},
|
||||
ip,
|
||||
userAgent
|
||||
);
|
||||
|
||||
this._track('register', providerUser.email, body.datafast_visitor_id).catch(
|
||||
(err) => {}
|
||||
);
|
||||
|
||||
await NewsletterService.register(providerUser.email);
|
||||
|
||||
try {
|
||||
if (providerInstance?.postRegistration) {
|
||||
await providerInstance.postRegistration(body.providerToken, create.id);
|
||||
}
|
||||
} catch (err) {
|
||||
// Don't fail registration if postRegistration fails
|
||||
}
|
||||
|
||||
return create.users[0].user;
|
||||
}
|
||||
|
||||
private async _track(
|
||||
name: string,
|
||||
email: string,
|
||||
datafast_visitor_id: string
|
||||
) {
|
||||
if (email && datafast_visitor_id && process.env.DATAFAST_API_KEY) {
|
||||
try {
|
||||
await fetch('https://datafa.st/api/v1/goals', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.DATAFAST_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
datafast_visitor_id: datafast_visitor_id,
|
||||
name: name,
|
||||
metadata: {
|
||||
email,
|
||||
},
|
||||
}),
|
||||
});
|
||||
} catch (err) {}
|
||||
}
|
||||
}
|
||||
|
||||
async forgot(email: string) {
|
||||
const user = await this._userService.getUserByEmail(email);
|
||||
if (!user || user.providerName !== Provider.LOCAL) {
|
||||
|
|
@ -244,7 +198,7 @@ export class AuthService {
|
|||
return this._userService.updatePassword(user.id, body.password);
|
||||
}
|
||||
|
||||
async activate(code: string, tracking: string) {
|
||||
async activate(code: string) {
|
||||
const user = AuthChecker.verifyJWT(code) as {
|
||||
id: string;
|
||||
activated: boolean;
|
||||
|
|
@ -257,7 +211,6 @@ export class AuthService {
|
|||
}
|
||||
await this._userService.activateUser(user.id);
|
||||
user.activated = true;
|
||||
this._track('register', user.email, tracking).catch((err) => {});
|
||||
await NewsletterService.register(user.email);
|
||||
return this.jwt(user as any);
|
||||
}
|
||||
|
|
@ -265,37 +218,18 @@ export class AuthService {
|
|||
return false;
|
||||
}
|
||||
|
||||
async resendActivationEmail(email: string) {
|
||||
const user = await this._userService.getUserByEmail(email);
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
if (user.activated) {
|
||||
throw new Error('Account is already activated');
|
||||
}
|
||||
|
||||
const jwt = await this.jwt(user);
|
||||
|
||||
await this._emailService.sendEmail(
|
||||
user.email,
|
||||
'Activate your account',
|
||||
`Click <a href="${process.env.FRONTEND_URL}/auth/activate/${jwt}">here</a> to activate your account`,
|
||||
'top'
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
oauthLink(provider: string, query?: any) {
|
||||
const providerInstance = this._providerManager.getProvider(provider);
|
||||
const providerInstance = ProvidersFactory.loadProvider(
|
||||
provider as Provider
|
||||
);
|
||||
return providerInstance.generateLink(query);
|
||||
}
|
||||
|
||||
async checkExists(provider: string, code: string, redirectUri?: string) {
|
||||
const providerInstance = this._providerManager.getProvider(provider);
|
||||
const token = await providerInstance.getToken(code, redirectUri);
|
||||
async checkExists(provider: string, code: string) {
|
||||
const providerInstance = ProvidersFactory.loadProvider(
|
||||
provider as Provider
|
||||
);
|
||||
const token = await providerInstance.getToken(code);
|
||||
const user = await providerInstance.getUser(token);
|
||||
if (!user) {
|
||||
throw new Error('Invalid user');
|
||||
|
|
@ -312,9 +246,6 @@ export class AuthService {
|
|||
}
|
||||
|
||||
private async jwt(user: User) {
|
||||
if (user.password) {
|
||||
delete user.password;
|
||||
}
|
||||
return AuthChecker.signJWT(user);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,7 @@ export class PoliciesGuard implements CanActivate {
|
|||
const request: Request = context.switchToHttp().getRequest();
|
||||
if (
|
||||
request.path.indexOf('/auth') > -1 ||
|
||||
request.path.indexOf('/auth') > -1 ||
|
||||
request.path.indexOf('/integrations/social-connect') > -1 ||
|
||||
request.path.indexOf('/integrations/provider') > -1
|
||||
request.path.indexOf('/stripe') > -1
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -44,10 +42,8 @@ export class PoliciesGuard implements CanActivate {
|
|||
// @ts-expect-error
|
||||
const { org }: { org: Organization } = request;
|
||||
|
||||
const refreshChannelId = typeof request.query?.refresh === 'string' ? request.query.refresh : undefined;
|
||||
|
||||
// @ts-ignore
|
||||
const ability = await this._authorizationService.check(org.id, org.createdAt, org.users[0].role, policyHandlers, refreshChannelId);
|
||||
const ability = await this._authorizationService.check(org.id, org.createdAt, org.users[0].role, policyHandlers);
|
||||
|
||||
const item = policyHandlers.find(
|
||||
(handler) => !this.execPolicyHandler(handler, ability)
|
||||
|
|
|
|||
|
|
@ -40,8 +40,7 @@ export class PermissionsService {
|
|||
orgId: string,
|
||||
created_at: Date,
|
||||
permission: 'USER' | 'ADMIN' | 'SUPERADMIN',
|
||||
requestedPermission: Array<[AuthorizationActions, Sections]>,
|
||||
refreshChannelId?: string
|
||||
requestedPermission: Array<[AuthorizationActions, Sections]>
|
||||
) {
|
||||
const { can, build } = new AbilityBuilder<
|
||||
Ability<[AuthorizationActions, Sections]>
|
||||
|
|
@ -66,20 +65,6 @@ export class PermissionsService {
|
|||
for (const [action, section] of requestedPermission) {
|
||||
// check for the amount of channels
|
||||
if (section === Sections.CHANNEL) {
|
||||
// Refreshing an existing channel doesn't add a new one, so skip the limit check
|
||||
// but only if the channel actually belongs to this org
|
||||
if (refreshChannelId) {
|
||||
const existingIntegration =
|
||||
await this._integrationService.getIntegrationById(
|
||||
orgId,
|
||||
refreshChannelId
|
||||
);
|
||||
if (existingIntegration) {
|
||||
can(action, section);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const totalChannels = (
|
||||
await this._integrationService.getIntegrationsList(orgId)
|
||||
).filter((f) => !f.refreshNeeded).length;
|
||||
|
|
|
|||
|
|
@ -1,34 +1,7 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
export abstract class AuthProviderAbstract {
|
||||
abstract generateLink(query?: any): Promise<string> | string;
|
||||
abstract getToken(code: string, redirectUri?: string): Promise<string>;
|
||||
abstract getUser(
|
||||
export interface ProvidersInterface {
|
||||
generateLink(query?: any): Promise<string> | string;
|
||||
getToken(code: string): Promise<string>;
|
||||
getUser(
|
||||
providerToken: string
|
||||
): Promise<{ email: string; id: string }> | false;
|
||||
async postRegistration(
|
||||
providerToken: string,
|
||||
orgId: string
|
||||
): Promise<void> {}
|
||||
}
|
||||
|
||||
export interface AuthProviderParams {
|
||||
provider: string;
|
||||
}
|
||||
|
||||
export function AuthProvider(params: AuthProviderParams) {
|
||||
return function (target: any) {
|
||||
Injectable()(target);
|
||||
|
||||
const existingMetadata =
|
||||
Reflect.getMetadata('auth-provider', AuthProviderAbstract) || [];
|
||||
|
||||
existingMetadata.push({ target, provider: params.provider });
|
||||
|
||||
Reflect.defineMetadata(
|
||||
'auth-provider',
|
||||
existingMetadata,
|
||||
AuthProviderAbstract
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,16 @@
|
|||
import {
|
||||
AuthProvider,
|
||||
AuthProviderAbstract,
|
||||
} from '@gitroom/backend/services/auth/providers.interface';
|
||||
import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.interface';
|
||||
import { NeynarAPIClient } from '@neynar/nodejs-sdk';
|
||||
|
||||
const client = new NeynarAPIClient({
|
||||
apiKey: process.env.NEYNAR_SECRET_KEY || '00000000-000-0000-000-000000000000',
|
||||
});
|
||||
|
||||
@AuthProvider({ provider: 'FARCASTER' })
|
||||
export class FarcasterProvider extends AuthProviderAbstract {
|
||||
export class FarcasterProvider implements ProvidersInterface {
|
||||
generateLink() {
|
||||
return '';
|
||||
}
|
||||
|
||||
async getToken(code: string, _redirectUri?: string) {
|
||||
async getToken(code: string) {
|
||||
const data = JSON.parse(Buffer.from(code, 'base64').toString());
|
||||
const status = await client.lookupSigner({ signerUuid: data.signer_uuid });
|
||||
if (status.status === 'approved') {
|
||||
|
|
@ -33,6 +29,11 @@ export class FarcasterProvider extends AuthProviderAbstract {
|
|||
};
|
||||
}
|
||||
|
||||
// const { client, oauth2 } = clientAndYoutube();
|
||||
// client.setCredentials({ access_token: providerToken });
|
||||
// const user = oauth2(client);
|
||||
// const { data } = await user.userinfo.get();
|
||||
|
||||
return {
|
||||
id: String('farcaster_' + status.fid),
|
||||
email: String('farcaster_' + status.fid),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
import {
|
||||
AuthProvider,
|
||||
AuthProviderAbstract,
|
||||
} from '@gitroom/backend/services/auth/providers.interface';
|
||||
import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.interface';
|
||||
|
||||
@AuthProvider({ provider: 'GITHUB' })
|
||||
export class GithubProvider extends AuthProviderAbstract {
|
||||
export class GithubProvider implements ProvidersInterface {
|
||||
generateLink(): string {
|
||||
return `https://github.com/login/oauth/authorize?client_id=${
|
||||
process.env.GITHUB_CLIENT_ID
|
||||
|
|
@ -13,7 +9,7 @@ export class GithubProvider extends AuthProviderAbstract {
|
|||
)}`;
|
||||
}
|
||||
|
||||
async getToken(code: string, _redirectUri?: string): Promise<string> {
|
||||
async getToken(code: string): Promise<string> {
|
||||
const { access_token } = await (
|
||||
await fetch('https://github.com/login/oauth/access_token', {
|
||||
method: 'POST',
|
||||
|
|
|
|||
|
|
@ -1,28 +1,45 @@
|
|||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { google } from 'googleapis';
|
||||
import {
|
||||
AuthProvider,
|
||||
AuthProviderAbstract,
|
||||
} from '@gitroom/backend/services/auth/providers.interface';
|
||||
import { OAuth2Client } from 'google-auth-library/build/src/auth/oauth2client';
|
||||
import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.interface';
|
||||
|
||||
const defaultRedirect = () =>
|
||||
`${process.env.FRONTEND_URL}/integrations/social/youtube`;
|
||||
|
||||
const makeClient = (redirectUri: string) =>
|
||||
new google.auth.OAuth2({
|
||||
const clientAndYoutube = () => {
|
||||
const client = new google.auth.OAuth2({
|
||||
clientId: process.env.YOUTUBE_CLIENT_ID,
|
||||
clientSecret: process.env.YOUTUBE_CLIENT_SECRET,
|
||||
redirectUri,
|
||||
redirectUri: `${process.env.FRONTEND_URL}/integrations/social/youtube`,
|
||||
});
|
||||
|
||||
@AuthProvider({ provider: 'GOOGLE' })
|
||||
export class GoogleProvider extends AuthProviderAbstract {
|
||||
generateLink(query?: { redirect_uri?: string }) {
|
||||
const redirectUri = query?.redirect_uri || defaultRedirect();
|
||||
return makeClient(redirectUri).generateAuthUrl({
|
||||
const youtube = (newClient: OAuth2Client) =>
|
||||
google.youtube({
|
||||
version: 'v3',
|
||||
auth: newClient,
|
||||
});
|
||||
|
||||
const youtubeAnalytics = (newClient: OAuth2Client) =>
|
||||
google.youtubeAnalytics({
|
||||
version: 'v2',
|
||||
auth: newClient,
|
||||
});
|
||||
|
||||
const oauth2 = (newClient: OAuth2Client) =>
|
||||
google.oauth2({
|
||||
version: 'v2',
|
||||
auth: newClient,
|
||||
});
|
||||
|
||||
return { client, youtube, oauth2, youtubeAnalytics };
|
||||
};
|
||||
|
||||
export class GoogleProvider implements ProvidersInterface {
|
||||
generateLink() {
|
||||
const state = makeId(7);
|
||||
const { client } = clientAndYoutube();
|
||||
return client.generateAuthUrl({
|
||||
access_type: 'online',
|
||||
prompt: 'consent',
|
||||
state: 'login',
|
||||
redirect_uri: redirectUri,
|
||||
state,
|
||||
redirect_uri: `${process.env.FRONTEND_URL}/integrations/social/youtube`,
|
||||
scope: [
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
|
|
@ -30,22 +47,21 @@ export class GoogleProvider extends AuthProviderAbstract {
|
|||
});
|
||||
}
|
||||
|
||||
async getToken(code: string, redirectUri?: string) {
|
||||
const client = makeClient(redirectUri || defaultRedirect());
|
||||
async getToken(code: string) {
|
||||
const { client, oauth2 } = clientAndYoutube();
|
||||
const { tokens } = await client.getToken(code);
|
||||
return tokens.access_token!;
|
||||
return tokens.access_token;
|
||||
}
|
||||
|
||||
async getUser(providerToken: string) {
|
||||
const client = makeClient(defaultRedirect());
|
||||
const { client, oauth2 } = clientAndYoutube();
|
||||
client.setCredentials({ access_token: providerToken });
|
||||
const { data } = await google
|
||||
.oauth2({ version: 'v2', auth: client })
|
||||
.userinfo.get();
|
||||
const user = oauth2(client);
|
||||
const { data } = await user.userinfo.get();
|
||||
|
||||
return {
|
||||
id: data.id!,
|
||||
email: data.email!,
|
||||
email: data.email,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,56 +1,66 @@
|
|||
import {
|
||||
AuthProvider,
|
||||
AuthProviderAbstract,
|
||||
} from '@gitroom/backend/services/auth/providers.interface';
|
||||
import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.interface';
|
||||
|
||||
@AuthProvider({ provider: 'GENERIC' })
|
||||
export class OauthProvider extends AuthProviderAbstract {
|
||||
private getConfig() {
|
||||
export class OauthProvider implements ProvidersInterface {
|
||||
private readonly authUrl: string;
|
||||
private readonly baseUrl: string;
|
||||
private readonly clientId: string;
|
||||
private readonly clientSecret: string;
|
||||
private readonly frontendUrl: string;
|
||||
private readonly tokenUrl: string;
|
||||
private readonly userInfoUrl: string;
|
||||
|
||||
constructor() {
|
||||
const {
|
||||
POSTIZ_OAUTH_AUTH_URL,
|
||||
POSTIZ_OAUTH_CLIENT_ID,
|
||||
POSTIZ_OAUTH_CLIENT_SECRET,
|
||||
POSTIZ_OAUTH_TOKEN_URL,
|
||||
POSTIZ_OAUTH_URL,
|
||||
POSTIZ_OAUTH_USERINFO_URL,
|
||||
FRONTEND_URL,
|
||||
} = process.env;
|
||||
|
||||
if (
|
||||
!POSTIZ_OAUTH_USERINFO_URL ||
|
||||
!POSTIZ_OAUTH_TOKEN_URL ||
|
||||
!POSTIZ_OAUTH_CLIENT_ID ||
|
||||
!POSTIZ_OAUTH_CLIENT_SECRET ||
|
||||
!POSTIZ_OAUTH_AUTH_URL ||
|
||||
!FRONTEND_URL
|
||||
) {
|
||||
throw new Error('POSTIZ_OAUTH environment variables are not set');
|
||||
}
|
||||
if (!POSTIZ_OAUTH_USERINFO_URL)
|
||||
throw new Error(
|
||||
'POSTIZ_OAUTH_USERINFO_URL environment variable is not set'
|
||||
);
|
||||
if (!POSTIZ_OAUTH_URL)
|
||||
throw new Error('POSTIZ_OAUTH_URL environment variable is not set');
|
||||
if (!POSTIZ_OAUTH_TOKEN_URL)
|
||||
throw new Error('POSTIZ_OAUTH_TOKEN_URL environment variable is not set');
|
||||
if (!POSTIZ_OAUTH_CLIENT_ID)
|
||||
throw new Error('POSTIZ_OAUTH_CLIENT_ID environment variable is not set');
|
||||
if (!POSTIZ_OAUTH_CLIENT_SECRET)
|
||||
throw new Error(
|
||||
'POSTIZ_OAUTH_CLIENT_SECRET environment variable is not set'
|
||||
);
|
||||
if (!POSTIZ_OAUTH_AUTH_URL)
|
||||
throw new Error('POSTIZ_OAUTH_AUTH_URL environment variable is not set');
|
||||
if (!FRONTEND_URL)
|
||||
throw new Error('FRONTEND_URL environment variable is not set');
|
||||
|
||||
return {
|
||||
authUrl: POSTIZ_OAUTH_AUTH_URL,
|
||||
clientId: POSTIZ_OAUTH_CLIENT_ID,
|
||||
clientSecret: POSTIZ_OAUTH_CLIENT_SECRET,
|
||||
tokenUrl: POSTIZ_OAUTH_TOKEN_URL,
|
||||
userInfoUrl: POSTIZ_OAUTH_USERINFO_URL,
|
||||
frontendUrl: FRONTEND_URL,
|
||||
};
|
||||
this.authUrl = POSTIZ_OAUTH_AUTH_URL;
|
||||
this.baseUrl = POSTIZ_OAUTH_URL;
|
||||
this.clientId = POSTIZ_OAUTH_CLIENT_ID;
|
||||
this.clientSecret = POSTIZ_OAUTH_CLIENT_SECRET;
|
||||
this.frontendUrl = FRONTEND_URL;
|
||||
this.tokenUrl = POSTIZ_OAUTH_TOKEN_URL;
|
||||
this.userInfoUrl = POSTIZ_OAUTH_USERINFO_URL;
|
||||
}
|
||||
|
||||
generateLink(): string {
|
||||
const { authUrl, clientId, frontendUrl } = this.getConfig();
|
||||
const params = new URLSearchParams({
|
||||
client_id: clientId,
|
||||
client_id: this.clientId,
|
||||
scope: 'openid profile email',
|
||||
response_type: 'code',
|
||||
redirect_uri: `${frontendUrl}/settings`,
|
||||
redirect_uri: `${this.frontendUrl}/settings`,
|
||||
});
|
||||
|
||||
return `${authUrl}?${params.toString()}`;
|
||||
return `${this.authUrl}?${params.toString()}`;
|
||||
}
|
||||
|
||||
async getToken(code: string, _redirectUri?: string): Promise<string> {
|
||||
const { tokenUrl, clientId, clientSecret, frontendUrl } = this.getConfig();
|
||||
const response = await fetch(`${tokenUrl}`, {
|
||||
async getToken(code: string): Promise<string> {
|
||||
const response = await fetch(`${this.tokenUrl}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
|
|
@ -58,10 +68,10 @@ export class OauthProvider extends AuthProviderAbstract {
|
|||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
client_id: this.clientId,
|
||||
client_secret: this.clientSecret,
|
||||
code,
|
||||
redirect_uri: `${frontendUrl}/settings`,
|
||||
redirect_uri: `${this.frontendUrl}/settings`,
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -75,8 +85,7 @@ export class OauthProvider extends AuthProviderAbstract {
|
|||
}
|
||||
|
||||
async getUser(access_token: string): Promise<{ email: string; id: string }> {
|
||||
const { userInfoUrl } = this.getConfig();
|
||||
const response = await fetch(`${userInfoUrl}`, {
|
||||
const response = await fetch(`${this.userInfoUrl}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
Accept: 'application/json',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
import { Provider } from '@prisma/client';
|
||||
import { GithubProvider } from '@gitroom/backend/services/auth/providers/github.provider';
|
||||
import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.interface';
|
||||
import { GoogleProvider } from '@gitroom/backend/services/auth/providers/google.provider';
|
||||
import { FarcasterProvider } from '@gitroom/backend/services/auth/providers/farcaster.provider';
|
||||
import { WalletProvider } from '@gitroom/backend/services/auth/providers/wallet.provider';
|
||||
import { OauthProvider } from '@gitroom/backend/services/auth/providers/oauth.provider';
|
||||
|
||||
export class ProvidersFactory {
|
||||
static loadProvider(provider: Provider): ProvidersInterface {
|
||||
switch (provider) {
|
||||
case Provider.GITHUB:
|
||||
return new GithubProvider();
|
||||
case Provider.GOOGLE:
|
||||
return new GoogleProvider();
|
||||
case Provider.FARCASTER:
|
||||
return new FarcasterProvider();
|
||||
case Provider.WALLET:
|
||||
return new WalletProvider();
|
||||
case Provider.GENERIC:
|
||||
return new OauthProvider();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { AuthProviderAbstract } from '@gitroom/backend/services/auth/providers.interface';
|
||||
|
||||
@Injectable()
|
||||
export class AuthProviderManager {
|
||||
constructor(private _moduleRef: ModuleRef) {}
|
||||
|
||||
getProvider(provider: string): AuthProviderAbstract {
|
||||
const metadata =
|
||||
Reflect.getMetadata('auth-provider', AuthProviderAbstract) || [];
|
||||
|
||||
const found = metadata.find(
|
||||
(m: any) => m.provider === provider
|
||||
);
|
||||
|
||||
if (!found) {
|
||||
throw new Error(`Auth provider ${provider} not found`);
|
||||
}
|
||||
|
||||
return this._moduleRef.get(found.target, { strict: false });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,16 @@
|
|||
import {
|
||||
AuthProvider,
|
||||
AuthProviderAbstract,
|
||||
} from '@gitroom/backend/services/auth/providers.interface';
|
||||
import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.interface';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
|
||||
import bs58 from 'bs58';
|
||||
import nacl from 'tweetnacl';
|
||||
|
||||
function hexToUint8Array(hex) {
|
||||
// Remove any potential "0x" prefix
|
||||
if (hex.startsWith('0x')) {
|
||||
hex = hex.slice(2);
|
||||
}
|
||||
|
||||
// Ensure the hex string has an even length
|
||||
if (hex.length % 2 !== 0) {
|
||||
throw new Error('Invalid hex string. It must have an even length.');
|
||||
}
|
||||
|
|
@ -20,15 +19,16 @@ function hexToUint8Array(hex) {
|
|||
const uint8Array = new Uint8Array(byteLength);
|
||||
|
||||
for (let i = 0; i < byteLength; i++) {
|
||||
// Get two characters from the hex string
|
||||
const byteHex = hex.substr(i * 2, 2);
|
||||
// Parse the two characters as a hexadecimal number
|
||||
uint8Array[i] = parseInt(byteHex, 16);
|
||||
}
|
||||
|
||||
return uint8Array;
|
||||
}
|
||||
|
||||
@AuthProvider({ provider: 'WALLET' })
|
||||
export class WalletProvider extends AuthProviderAbstract {
|
||||
export class WalletProvider implements ProvidersInterface {
|
||||
async generateLink(params: { publicKey: string }) {
|
||||
if (!params.publicKey) {
|
||||
return;
|
||||
|
|
@ -40,7 +40,7 @@ export class WalletProvider extends AuthProviderAbstract {
|
|||
return challenge;
|
||||
}
|
||||
|
||||
async getToken(code: string, _redirectUri?: string) {
|
||||
async getToken(code: string) {
|
||||
const { publicKey, challenge, signature } = JSON.parse(
|
||||
Buffer.from(code, 'base64').toString()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,11 @@
|
|||
import { HttpStatus, Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
|
||||
import { OAuthService } from '@gitroom/nestjs-libraries/database/prisma/oauth/oauth.service';
|
||||
import { HttpForbiddenException } from '@gitroom/nestjs-libraries/services/exception.filter';
|
||||
|
||||
@Injectable()
|
||||
export class PublicAuthMiddleware implements NestMiddleware {
|
||||
constructor(
|
||||
private _organizationService: OrganizationService,
|
||||
private _oauthService: OAuthService
|
||||
) {}
|
||||
constructor(private _organizationService: OrganizationService) {}
|
||||
async use(req: Request, res: Response, next: NextFunction) {
|
||||
const auth = (req.headers.authorization ||
|
||||
req.headers.Authorization) as string;
|
||||
|
|
@ -18,44 +14,21 @@ export class PublicAuthMiddleware implements NestMiddleware {
|
|||
return;
|
||||
}
|
||||
try {
|
||||
if (auth.startsWith('pos_')) {
|
||||
const authorization = await this._oauthService.getOrgByOAuthToken(auth);
|
||||
if (!authorization) {
|
||||
res
|
||||
.status(HttpStatus.UNAUTHORIZED)
|
||||
.json({ msg: 'Invalid OAuth token' });
|
||||
return;
|
||||
}
|
||||
|
||||
const org = authorization.organization;
|
||||
if (!!process.env.STRIPE_SECRET_KEY && !org.subscription) {
|
||||
res
|
||||
.status(HttpStatus.UNAUTHORIZED)
|
||||
.json({ msg: 'No subscription found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
req.org = { ...org, users: [{ users: { role: 'SUPERADMIN' } }] };
|
||||
} else {
|
||||
const org = await this._organizationService.getOrgByApiKey(auth);
|
||||
if (!org) {
|
||||
res
|
||||
.status(HttpStatus.UNAUTHORIZED)
|
||||
.json({ msg: 'Invalid API key' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!!process.env.STRIPE_SECRET_KEY && !org.subscription) {
|
||||
res
|
||||
.status(HttpStatus.UNAUTHORIZED)
|
||||
.json({ msg: 'No subscription found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
req.org = { ...org, users: [{ users: { role: 'SUPERADMIN' } }] };
|
||||
const org = await this._organizationService.getOrgByApiKey(auth);
|
||||
if (!org) {
|
||||
res.status(HttpStatus.UNAUTHORIZED).json({ msg: 'Invalid API key' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!!process.env.STRIPE_SECRET_KEY && !org.subscription) {
|
||||
res
|
||||
.status(HttpStatus.UNAUTHORIZED)
|
||||
.json({ msg: 'No subscription found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
req.org = { ...org, users: [{ users: { role: 'SUPERADMIN' } }] };
|
||||
} catch (err) {
|
||||
throw new HttpForbiddenException();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { CommandModule as ExternalCommandModule } from 'nestjs-command';
|
||||
import { CheckStars } from './tasks/check.stars';
|
||||
import { DatabaseModule } from '@gitroom/nestjs-libraries/database/prisma/database.module';
|
||||
import { RefreshTokens } from './tasks/refresh.tokens';
|
||||
import { BullMqModule } from '@gitroom/nestjs-libraries/bull-mq-transport-new/bull.mq.module';
|
||||
import { ConfigurationTask } from './tasks/configuration';
|
||||
import { AgentRun } from './tasks/agent.run';
|
||||
import { AgentModule } from '@gitroom/nestjs-libraries/agent/agent.module';
|
||||
|
||||
@Module({
|
||||
imports: [ExternalCommandModule, DatabaseModule, AgentModule],
|
||||
imports: [ExternalCommandModule, DatabaseModule, BullMqModule, AgentModule],
|
||||
controllers: [],
|
||||
providers: [RefreshTokens, ConfigurationTask, AgentRun],
|
||||
providers: [CheckStars, RefreshTokens, ConfigurationTask, AgentRun],
|
||||
get exports() {
|
||||
return [...this.imports, ...this.providers];
|
||||
},
|
||||
|
|
|
|||
52
apps/commands/src/tasks/check.stars.ts
Normal file
52
apps/commands/src/tasks/check.stars.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { Command, Positional } from 'nestjs-command';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
|
||||
@Injectable()
|
||||
export class CheckStars {
|
||||
constructor(private _workerServiceProducer: BullMqClient) {}
|
||||
@Command({
|
||||
command: 'sync:stars <login>',
|
||||
describe: 'Sync stars for a login',
|
||||
})
|
||||
async create(
|
||||
@Positional({
|
||||
name: 'login',
|
||||
describe: 'login {owner}/{repo}',
|
||||
type: 'string',
|
||||
})
|
||||
login: string
|
||||
) {
|
||||
this._workerServiceProducer
|
||||
.emit('check_stars', { payload: { login } })
|
||||
.subscribe();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Command({
|
||||
command: 'sync:all_stars <login>',
|
||||
describe: 'Sync all stars for a login',
|
||||
})
|
||||
async syncAllStars(
|
||||
@Positional({
|
||||
name: 'login',
|
||||
describe: 'login {owner}/{repo}',
|
||||
type: 'string',
|
||||
})
|
||||
login: string
|
||||
) {
|
||||
this._workerServiceProducer
|
||||
.emit('sync_all_stars', { payload: { login } })
|
||||
.subscribe();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Command({
|
||||
command: 'sync:trending',
|
||||
describe: 'Sync trending',
|
||||
})
|
||||
async syncTrending() {
|
||||
this._workerServiceProducer.emit('sync_trending', {}).subscribe();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
"collection": "@nestjs/schematics",
|
||||
"monorepo": false,
|
||||
"sourceRoot": "src",
|
||||
"entryFile": "../../dist/orchestrator/apps/orchestrator/src/main",
|
||||
"entryFile": "../../dist/cron/apps/cron/src/main",
|
||||
"language": "ts",
|
||||
"generateOptions": {
|
||||
"spec": false
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "postiz-orchestrator",
|
||||
"name": "postiz-cron",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dev": "dotenv -e ../../.env -- nest start --watch --entryFile=./apps/orchestrator/src/main",
|
||||
"dev": "dotenv -e ../../.env -- nest start --watch --entryFile=./apps/cron/src/main",
|
||||
"build": "cross-env NODE_ENV=production nest build",
|
||||
"start": "dotenv -e ../../.env -- node --experimental-require-module ./dist/apps/orchestrator/src/main.js",
|
||||
"pm2": "pm2 start pnpm --name orchestrator -- start"
|
||||
"start": "dotenv -e ../../.env -- node --experimental-require-module ./dist/apps/cron/src/main.js",
|
||||
"pm2": "pm2 start pnpm --name cron -- start"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
20
apps/cron/src/cron.module.ts
Normal file
20
apps/cron/src/cron.module.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { DatabaseModule } from '@gitroom/nestjs-libraries/database/prisma/database.module';
|
||||
import { BullMqModule } from '@gitroom/nestjs-libraries/bull-mq-transport-new/bull.mq.module';
|
||||
import { SentryModule } from '@sentry/nestjs/setup';
|
||||
import { FILTER } from '@gitroom/nestjs-libraries/sentry/sentry.exception';
|
||||
import { CheckMissingQueues } from '@gitroom/cron/tasks/check.missing.queues';
|
||||
import { PostNowPendingQueues } from '@gitroom/cron/tasks/post.now.pending.queues';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
SentryModule.forRoot(),
|
||||
DatabaseModule,
|
||||
ScheduleModule.forRoot(),
|
||||
BullMqModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [FILTER, CheckMissingQueues, PostNowPendingQueues],
|
||||
})
|
||||
export class CronModule {}
|
||||
12
apps/cron/src/main.ts
Normal file
12
apps/cron/src/main.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { initializeSentry } from '@gitroom/nestjs-libraries/sentry/initialize.sentry';
|
||||
initializeSentry('cron');
|
||||
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { CronModule } from './cron.module';
|
||||
|
||||
async function start() {
|
||||
// some comment again
|
||||
await NestFactory.createApplicationContext(CronModule);
|
||||
}
|
||||
|
||||
start();
|
||||
0
apps/cron/src/tasks/.gitkeep
Normal file
0
apps/cron/src/tasks/.gitkeep
Normal file
45
apps/cron/src/tasks/check.missing.queues.ts
Normal file
45
apps/cron/src/tasks/check.missing.queues.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
@Injectable()
|
||||
export class CheckMissingQueues {
|
||||
constructor(
|
||||
private _postService: PostsService,
|
||||
private _workerServiceProducer: BullMqClient
|
||||
) {}
|
||||
@Cron('0 * * * *')
|
||||
async handleCron() {
|
||||
const list = await this._postService.searchForMissingThreeHoursPosts();
|
||||
const notExists = (
|
||||
await Promise.all(
|
||||
list.map(async (p) => ({
|
||||
id: p.id,
|
||||
publishDate: p.publishDate,
|
||||
isJob:
|
||||
['delayed', 'waiting'].indexOf(
|
||||
await this._workerServiceProducer
|
||||
.getQueue('post')
|
||||
.getJobState(p.id)
|
||||
) > -1,
|
||||
}))
|
||||
)
|
||||
).filter((p) => !p.isJob);
|
||||
|
||||
|
||||
for (const job of notExists) {
|
||||
this._workerServiceProducer.emit('post', {
|
||||
id: job.id,
|
||||
options: {
|
||||
delay: dayjs(job.publishDate).diff(dayjs(), 'millisecond'),
|
||||
},
|
||||
payload: {
|
||||
id: job.id,
|
||||
delay: dayjs(job.publishDate).diff(dayjs(), 'millisecond'),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
43
apps/cron/src/tasks/post.now.pending.queues.ts
Normal file
43
apps/cron/src/tasks/post.now.pending.queues.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
|
||||
@Injectable()
|
||||
export class PostNowPendingQueues {
|
||||
constructor(
|
||||
private _postService: PostsService,
|
||||
private _workerServiceProducer: BullMqClient
|
||||
) {}
|
||||
@Cron('*/16 * * * *')
|
||||
async handleCron() {
|
||||
const list = await this._postService.checkPending15minutesBack();
|
||||
const notExists = (
|
||||
await Promise.all(
|
||||
list.map(async (p) => ({
|
||||
id: p.id,
|
||||
publishDate: p.publishDate,
|
||||
isJob:
|
||||
['delayed', 'waiting'].indexOf(
|
||||
await this._workerServiceProducer
|
||||
.getQueue('post')
|
||||
.getJobState(p.id)
|
||||
) > -1,
|
||||
}))
|
||||
)
|
||||
).filter((p) => !p.isJob);
|
||||
|
||||
for (const job of notExists) {
|
||||
this._workerServiceProducer.emit('post', {
|
||||
id: job.id,
|
||||
options: {
|
||||
delay: 0,
|
||||
},
|
||||
payload: {
|
||||
id: job.id,
|
||||
delay: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
13
apps/cron/tsconfig.json
Normal file
13
apps/cron/tsconfig.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noLib": false,
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
}
|
||||
}
|
||||
37
apps/extension/manifest.dev.json
Normal file → Executable file
37
apps/extension/manifest.dev.json
Normal file → Executable file
|
|
@ -1,30 +1,15 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Postiz",
|
||||
"version": "2.0.0",
|
||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqtH6qclAsfFf6qbUKfPmhBbfycGrt13+0h6ti/olniCGnjQjhkVVTnURfLFz+v+842Ee+pAS5HBEXo57dQ9xUtwFGXnavVR+myjN+Un9NIfFyYmYEBvLrinclsMJBwWMM8JkhxKuaOagxp1hqGgNAO4C0bzE3YN/SPoTjNpGU8TGm/ENZ/TDUneZyyVM5HEEmOTZEmjmy9FJaxbzGmZ2rixNO45pkjXMFp8+/XrFSNiCqNZt6LQNIqL5SfVIRUKGBjE3OG/gtahVToBdlXi5yzP1uYE0Qs4grJ/T1rUUzTXFAQa7heWA9mskf0xAMEtTSED4N9bZ4sF8cf5J+SGGlwIDAQAB",
|
||||
"description": "Postiz browser extension for social media scheduling",
|
||||
"action": {
|
||||
"default_icon": "public/dev-icon-32.png",
|
||||
"default_popup": "src/pages/popup/index.html"
|
||||
},
|
||||
"icons": {
|
||||
"32": "icon-32.png",
|
||||
"128": "icon-128.png"
|
||||
"128": "public/dev-icon-128.png"
|
||||
},
|
||||
"permissions": [
|
||||
"cookies",
|
||||
"alarms",
|
||||
"storage"
|
||||
],
|
||||
"host_permissions": [
|
||||
"*://*.skool.com/*"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js",
|
||||
"type": "module"
|
||||
},
|
||||
"externally_connectable": {
|
||||
"matches": [
|
||||
"http://localhost/*",
|
||||
"https://localhost/*",
|
||||
"https://*.postiz.com/*"
|
||||
]
|
||||
}
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["contentStyle.css", "dev-icon-128.png", "dev-icon-32.png"],
|
||||
"matches": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,31 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Postiz",
|
||||
"version": "2.0.0",
|
||||
"description": "Postiz browser extension for social media scheduling",
|
||||
"description": "Your ultimate social media scheduling tool",
|
||||
"options_ui": {
|
||||
"page": "src/pages/options/index.html"
|
||||
},
|
||||
"action": {
|
||||
"default_popup": "src/pages/popup/index.html",
|
||||
"default_icon": {
|
||||
"32": "icon-32.png"
|
||||
}
|
||||
},
|
||||
"icons": {
|
||||
"32": "icon-32.png",
|
||||
"128": "icon-128.png"
|
||||
},
|
||||
"permissions": [
|
||||
"cookies",
|
||||
"alarms",
|
||||
"storage"
|
||||
"permissions": ["activeTab", "cookies", "tabs"],
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["http://*/*", "https://*/*", "<all_urls>"],
|
||||
"js": ["src/pages/content/index.tsx"],
|
||||
"css": ["contentStyle.css"]
|
||||
}
|
||||
],
|
||||
"host_permissions": [
|
||||
"*://*.skool.com/*"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js",
|
||||
"type": "module"
|
||||
},
|
||||
"externally_connectable": {
|
||||
"matches": [
|
||||
"http://localhost/*",
|
||||
"https://localhost/*",
|
||||
"https://*.postiz.com/*"
|
||||
]
|
||||
}
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["contentStyle.css", "icon-128.png", "icon-32.png"],
|
||||
"matches": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
16
apps/extension/nodemon.chrome.json
Normal file
16
apps/extension/nodemon.chrome.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"env": {
|
||||
"__DEV__": "true"
|
||||
},
|
||||
"watch": [
|
||||
"src",
|
||||
"utils",
|
||||
"vite.config.base.ts",
|
||||
"vite.config.chrome.ts",
|
||||
"manifest.json",
|
||||
"manifest.dev.json"
|
||||
],
|
||||
"ext": "tsx,css,html,ts,json",
|
||||
"ignore": ["src/**/*.spec.ts"],
|
||||
"exec": "vite build --config vite.config.chrome.ts --mode development"
|
||||
}
|
||||
16
apps/extension/nodemon.firefox.json
Normal file
16
apps/extension/nodemon.firefox.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"env": {
|
||||
"__DEV__": "true"
|
||||
},
|
||||
"watch": [
|
||||
"src",
|
||||
"utils",
|
||||
"vite.config.base.ts",
|
||||
"vite.config.firefox.ts",
|
||||
"manifest.json",
|
||||
"manifest.dev.json"
|
||||
],
|
||||
"ext": "tsx,css,html,ts,json",
|
||||
"ignore": ["src/**/*.spec.ts"],
|
||||
"exec": "vite build --config vite.config.firefox.ts --mode development"
|
||||
}
|
||||
|
|
@ -1,10 +1,14 @@
|
|||
{
|
||||
"name": "postiz-extension",
|
||||
"version": "2.0.0",
|
||||
"description": "Postiz browser extension for cookie-based platform authentication",
|
||||
"version": "1.0.3",
|
||||
"description": "A simple chrome & firefox extension template with Vite, React, TypeScript and Tailwind CSS.",
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && vite build && cp manifest.json dist/manifest.json && cd dist && zip -r ../extension.zip .",
|
||||
"dev": "rm -rf dist && HOT_RELOAD_EXTENSION_VITE_PORT=8081 NODE_ENV=development dotenv -e ../../.env -- vite build --config vite.config.chrome.ts --mode development --watch"
|
||||
"build": "rm -rf dist && vite build --config vite.config.chrome.ts && zip -r extension.zip dist",
|
||||
"build:chrome": "vite build --config vite.config.chrome.ts",
|
||||
"build:firefox": "vite build --config vite.config.firefox.ts",
|
||||
"dev": "rm -rf dist && dotenv -e ../../.env -- vite build --config vite.config.chrome.ts --mode development --watch",
|
||||
"dev:chrome": "nodemon --config nodemon.chrome.json",
|
||||
"dev:firefox": "nodemon --config nodemon.firefox.json"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
|
|
|||
0
apps/extension/public/contentStyle.css
Normal file
0
apps/extension/public/contentStyle.css
Normal file
BIN
apps/extension/public/dev-icon-128.png
Normal file
BIN
apps/extension/public/dev-icon-128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
BIN
apps/extension/public/dev-icon-32.png
Executable file
BIN
apps/extension/public/dev-icon-32.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1 KiB |
7
apps/extension/src/assets/img/logo.svg
Normal file
7
apps/extension/src/assets/img/logo.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
|
||||
<g fill="#61DAFB">
|
||||
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
|
||||
<circle cx="420.9" cy="296.5" r="45.7"/>
|
||||
<path d="M520.5 78.1z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
13
apps/extension/src/assets/styles/tailwind.css
Normal file
13
apps/extension/src/assets/styles/tailwind.css
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@theme {
|
||||
--animate-spin-slow: spin 20s linear infinite;
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
import { ExtensionRequest, GetCookiesResponse, ProviderInfo, StoredRefreshEntry } from './types/messages';
|
||||
import { getAllProviders, getProvider } from './providers/provider.registry';
|
||||
import { CookieProvider } from './providers/cookie-provider.interface';
|
||||
|
||||
const EXTENSION_VERSION = '2.0.0';
|
||||
const REFRESH_ALARM_NAME = 'cookie-refresh';
|
||||
const STORAGE_KEY = 'refreshEntries';
|
||||
|
||||
const ALLOWED_ORIGIN_PATTERNS = [
|
||||
/^https?:\/\/localhost(:\d+)?$/,
|
||||
/^https?:\/\/([a-z0-9-]+\.)*postiz\.com$/,
|
||||
];
|
||||
|
||||
function isOriginAllowed(origin: string | undefined): boolean {
|
||||
if (!origin) return false;
|
||||
return ALLOWED_ORIGIN_PATTERNS.some((pattern) => pattern.test(origin));
|
||||
}
|
||||
|
||||
async function extractCookies(provider: CookieProvider): Promise<GetCookiesResponse> {
|
||||
const allCookies = await chrome.cookies.getAll({ url: provider.url });
|
||||
|
||||
const extracted: Record<string, string> = {};
|
||||
const missingRequired: string[] = [];
|
||||
|
||||
for (const def of provider.cookies) {
|
||||
const found = allCookies.find((c) => c.name === def.name);
|
||||
if (found) {
|
||||
extracted[def.name] = found.value;
|
||||
} else if (def.required) {
|
||||
missingRequired.push(def.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingRequired.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
provider: provider.identifier,
|
||||
error: `Missing required cookies: ${missingRequired.join(', ')}. User may need to log in to ${provider.name}.`,
|
||||
missingCookies: missingRequired,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: provider.identifier,
|
||||
cookies: extracted,
|
||||
};
|
||||
}
|
||||
|
||||
// --- Refresh Token Storage Helpers ---
|
||||
|
||||
async function getStoredEntries(): Promise<Record<string, StoredRefreshEntry>> {
|
||||
const result = await chrome.storage.local.get(STORAGE_KEY);
|
||||
return result[STORAGE_KEY] || {};
|
||||
}
|
||||
|
||||
async function setStoredEntries(entries: Record<string, StoredRefreshEntry>): Promise<void> {
|
||||
await chrome.storage.local.set({ [STORAGE_KEY]: entries });
|
||||
}
|
||||
|
||||
async function ensureAlarm(): Promise<void> {
|
||||
const existing = await chrome.alarms.get(REFRESH_ALARM_NAME);
|
||||
if (!existing) {
|
||||
chrome.alarms.create(REFRESH_ALARM_NAME, { periodInMinutes: 1440 });
|
||||
}
|
||||
}
|
||||
|
||||
async function clearAlarmIfEmpty(): Promise<void> {
|
||||
const entries = await getStoredEntries();
|
||||
if (Object.keys(entries).length === 0) {
|
||||
await chrome.alarms.clear(REFRESH_ALARM_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Background Cookie Refresh ---
|
||||
|
||||
async function refreshAllCookies(): Promise<void> {
|
||||
const entries = await getStoredEntries();
|
||||
for (const [integrationId, entry] of Object.entries(entries)) {
|
||||
try {
|
||||
const provider = getProvider(entry.provider);
|
||||
if (!provider) continue;
|
||||
|
||||
const cookieResult = await extractCookies(provider);
|
||||
if (!cookieResult.success) continue;
|
||||
|
||||
const base64Cookies = btoa(JSON.stringify(cookieResult.cookies));
|
||||
|
||||
await fetch(`${entry.backendUrl}/integrations/extension-refresh`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ jwt: entry.jwt, cookies: base64Cookies }),
|
||||
});
|
||||
} catch {
|
||||
// Silently skip — will retry next cycle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Alarm Listener ---
|
||||
|
||||
chrome.alarms.onAlarm.addListener((alarm) => {
|
||||
if (alarm.name === REFRESH_ALARM_NAME) {
|
||||
refreshAllCookies();
|
||||
}
|
||||
});
|
||||
|
||||
// --- Ensure alarm on startup ---
|
||||
|
||||
(async () => {
|
||||
const entries = await getStoredEntries();
|
||||
if (Object.keys(entries).length > 0) {
|
||||
await ensureAlarm();
|
||||
}
|
||||
})();
|
||||
|
||||
// --- Message Listener ---
|
||||
|
||||
chrome.runtime.onMessageExternal.addListener(
|
||||
(
|
||||
message: ExtensionRequest,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
sendResponse: (response: unknown) => void
|
||||
) => {
|
||||
const origin = sender.origin ?? sender.url;
|
||||
if (!isOriginAllowed(origin)) {
|
||||
sendResponse({ error: 'Unauthorized origin' });
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (message.type) {
|
||||
case 'PING': {
|
||||
sendResponse({ status: 'ok', version: EXTENSION_VERSION });
|
||||
break;
|
||||
}
|
||||
|
||||
case 'GET_PROVIDERS': {
|
||||
const providers = getAllProviders();
|
||||
const providerInfos: ProviderInfo[] = providers.map((p) => ({
|
||||
identifier: p.identifier,
|
||||
name: p.name,
|
||||
url: p.url,
|
||||
cookieNames: p.cookies.map((c) => c.name),
|
||||
}));
|
||||
sendResponse({ providers: providerInfos });
|
||||
break;
|
||||
}
|
||||
|
||||
case 'GET_COOKIES': {
|
||||
const provider = getProvider(message.provider);
|
||||
if (!provider) {
|
||||
sendResponse({
|
||||
success: false,
|
||||
provider: message.provider,
|
||||
error: `Unknown provider: ${message.provider}`,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
extractCookies(provider)
|
||||
.then((result) => sendResponse(result))
|
||||
.catch((err) =>
|
||||
sendResponse({
|
||||
success: false,
|
||||
provider: message.provider,
|
||||
error: `Failed to extract cookies: ${err.message}`,
|
||||
})
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'STORE_REFRESH_TOKEN': {
|
||||
(async () => {
|
||||
const entries = await getStoredEntries();
|
||||
entries[message.integrationId] = {
|
||||
jwt: message.jwt,
|
||||
backendUrl: message.backendUrl,
|
||||
provider: message.provider,
|
||||
};
|
||||
await setStoredEntries(entries);
|
||||
await ensureAlarm();
|
||||
sendResponse({ success: true });
|
||||
})().catch(() => sendResponse({ success: false }));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'REMOVE_REFRESH_TOKEN': {
|
||||
(async () => {
|
||||
const entries = await getStoredEntries();
|
||||
delete entries[message.integrationId];
|
||||
await setStoredEntries(entries);
|
||||
await clearAlarmIfEmpty();
|
||||
sendResponse({ success: true });
|
||||
})().catch(() => sendResponse({ success: false }));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
default: {
|
||||
sendResponse({ error: `Unknown message type: ${(message as any).type}` });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
11
apps/extension/src/global.d.ts
vendored
Normal file
11
apps/extension/src/global.d.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
declare module '*.svg' {
|
||||
import React = require('react');
|
||||
export const ReactComponent: React.SFC<React.SVGProps<SVGSVGElement>>;
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*.json' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
10
apps/extension/src/locales/en/messages.json
Normal file
10
apps/extension/src/locales/en/messages.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extName": {
|
||||
"message": "name in src/locales/en/messages.json",
|
||||
"description": "Extension name"
|
||||
},
|
||||
"extDescription": {
|
||||
"message": "description in src/locales/en/messages.json",
|
||||
"description": "Extension description"
|
||||
}
|
||||
}
|
||||
37
apps/extension/src/pages/background/index.ts
Normal file
37
apps/extension/src/pages/background/index.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { fetchRequestUtil } from '@gitroom/extension/utils/request.util';
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
|
||||
if (request.action === 'makeHttpRequest') {
|
||||
fetchRequestUtil(request).then((response) => {
|
||||
sendResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
if (request.action === 'loadStorage') {
|
||||
chrome.storage.local.get([request.key], function (storage) {
|
||||
sendResponse(storage[request.key]);
|
||||
});
|
||||
}
|
||||
|
||||
if (request.action === 'saveStorage') {
|
||||
chrome.storage.local.set({ [request.key]: request.value }, function () {
|
||||
sendResponse({ success: true });
|
||||
});
|
||||
}
|
||||
|
||||
if (request.action === 'loadCookie') {
|
||||
chrome.cookies.get(
|
||||
{
|
||||
url: import.meta.env?.FRONTEND_URL || process?.env?.FRONTEND_URL,
|
||||
name: request.cookieName,
|
||||
},
|
||||
function (cookies) {
|
||||
sendResponse(cookies?.value);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
115
apps/extension/src/pages/content/elements/action.component.tsx
Normal file
115
apps/extension/src/pages/content/elements/action.component.tsx
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import { FC, memo, useCallback, useEffect, useState } from 'react';
|
||||
import { ProviderInterface } from '@gitroom/extension/providers/provider.interface';
|
||||
import { fetchCookie } from '@gitroom/extension/utils/load.cookie';
|
||||
|
||||
const Comp: FC<{ removeModal: () => void; platform: string; style: string }> = (
|
||||
props
|
||||
) => {
|
||||
const load = async () => {
|
||||
const cookie = await fetchCookie(`auth`);
|
||||
if (document.querySelector('iframe#modal-postiz')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
|
||||
div.style.position = 'fixed';
|
||||
div.style.top = '0';
|
||||
div.style.left = '0';
|
||||
div.style.zIndex = '9999';
|
||||
div.style.width = '100%';
|
||||
div.style.height = '100%';
|
||||
div.style.border = 'none';
|
||||
div.style.overflow = 'hidden';
|
||||
document.body.appendChild(div);
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.style.backgroundColor = 'transparent';
|
||||
// @ts-ignore
|
||||
iframe.allowTransparency = 'true';
|
||||
iframe.src =
|
||||
(import.meta.env?.FRONTEND_URL || process?.env?.FRONTEND_URL) +
|
||||
`/modal/${props.style}/${props.platform}?loggedAuth=${cookie}`;
|
||||
iframe.id = 'modal-postiz';
|
||||
iframe.style.width = '100%';
|
||||
iframe.style.height = '100%';
|
||||
iframe.style.position = 'fixed';
|
||||
iframe.style.top = '0';
|
||||
iframe.style.left = '0';
|
||||
iframe.style.zIndex = '9999';
|
||||
iframe.style.border = 'none';
|
||||
div.appendChild(iframe);
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data.action === 'closeIframe') {
|
||||
const iframe = document.querySelector('iframe#modal-postiz');
|
||||
if (iframe) {
|
||||
props.removeModal();
|
||||
div.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, []);
|
||||
return <></>;
|
||||
};
|
||||
export const ActionComponent: FC<{
|
||||
target: Node;
|
||||
keyIndex: number;
|
||||
actionType: string;
|
||||
provider: ProviderInterface;
|
||||
wrap: boolean;
|
||||
selector: string;
|
||||
}> = memo((props) => {
|
||||
const { wrap, provider, selector, target, actionType } = props;
|
||||
const [modal, showModal] = useState(false);
|
||||
const handle = useCallback(async (e: any) => {
|
||||
showModal(true);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const blockingDiv = document.createElement('div');
|
||||
if (document.querySelector(`.${selector}`)) {
|
||||
console.log('already exists');
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
// @ts-ignore
|
||||
const targetInformation = target.getBoundingClientRect();
|
||||
blockingDiv.style.position = 'absolute';
|
||||
blockingDiv.id = 'blockingDiv';
|
||||
blockingDiv.style.cursor = 'pointer';
|
||||
blockingDiv.style.top = `${targetInformation.top}px`;
|
||||
blockingDiv.style.left = `${targetInformation.left}px`;
|
||||
blockingDiv.style.width = `${targetInformation.width}px`;
|
||||
blockingDiv.style.height = `${targetInformation.height}px`;
|
||||
blockingDiv.style.zIndex = '9999';
|
||||
blockingDiv.className = selector;
|
||||
|
||||
document.body.appendChild(blockingDiv);
|
||||
blockingDiv.addEventListener('click', handle);
|
||||
}, 1000);
|
||||
return () => {
|
||||
blockingDiv.removeEventListener('click', handle);
|
||||
blockingDiv.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="g-wrapper" style={{ position: 'relative' }}>
|
||||
<div className="absolute start-0 top-0 z-[9999] w-full h-full" />
|
||||
{modal && (
|
||||
<Comp
|
||||
platform={provider.identifier}
|
||||
style={provider.style}
|
||||
removeModal={() => showModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
11
apps/extension/src/pages/content/index.tsx
Normal file
11
apps/extension/src/pages/content/index.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { createRoot } from 'react-dom/client';
|
||||
import './style.css';
|
||||
import { MainContent } from '@gitroom/extension/pages/content/main.content';
|
||||
const div = document.createElement('div');
|
||||
div.id = '__root';
|
||||
document.body.appendChild(div);
|
||||
|
||||
const rootContainer = document.querySelector('#__root');
|
||||
if (!rootContainer) throw new Error("Can't find Content root element");
|
||||
const root = createRoot(rootContainer);
|
||||
root.render(<MainContent />);
|
||||
191
apps/extension/src/pages/content/main.content.tsx
Normal file
191
apps/extension/src/pages/content/main.content.tsx
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import {
|
||||
FC,
|
||||
Fragment,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { ProviderList } from '@gitroom/extension/providers/provider.list';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { ActionComponent } from '@gitroom/extension/pages/content/elements/action.component';
|
||||
|
||||
// Define a type to track elements with their action types
|
||||
interface ActionElement {
|
||||
element: HTMLElement;
|
||||
actionType: string;
|
||||
}
|
||||
|
||||
export const MainContent: FC = () => {
|
||||
return <MainContentInner />;
|
||||
};
|
||||
|
||||
export const MainContentInner: FC = (props) => {
|
||||
const [actionElements, setActionElements] = useState<ActionElement[]>([]);
|
||||
const actionSetRef = useRef(new Map<HTMLElement, string>());
|
||||
const provider = useMemo(() => {
|
||||
return ProviderList.find((p) => {
|
||||
return p.baseUrl.indexOf(new URL(window.location.href).hostname) > -1;
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!provider) return;
|
||||
|
||||
// Helper to scan DOM for existing matching elements
|
||||
const scanDOMForExistingMatches = () => {
|
||||
const action = { selector: provider.element, type: 'post' };
|
||||
const matches = document.querySelectorAll(action.selector);
|
||||
matches.forEach((match) => {
|
||||
const htmlMatch = match as HTMLElement;
|
||||
if (!actionSetRef.current.has(htmlMatch)) {
|
||||
actionSetRef.current.set(htmlMatch, action.type);
|
||||
}
|
||||
});
|
||||
|
||||
// Update state
|
||||
const elements: ActionElement[] = [];
|
||||
actionSetRef.current.forEach((actionType, element) => {
|
||||
elements.push({ element, actionType });
|
||||
});
|
||||
setActionElements(elements);
|
||||
};
|
||||
|
||||
// Initial scan before observing
|
||||
scanDOMForExistingMatches();
|
||||
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
let addedSomething = false;
|
||||
let removedSomething = false;
|
||||
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === 'childList') {
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const el = node as HTMLElement;
|
||||
|
||||
const action = { selector: provider.element, type: 'post' };
|
||||
if (
|
||||
el.matches?.(action.selector) &&
|
||||
!actionSetRef.current.has(el)
|
||||
) {
|
||||
actionSetRef.current.set(el, action.type);
|
||||
addedSomething = true;
|
||||
}
|
||||
|
||||
if (el.querySelectorAll) {
|
||||
const matches = el.querySelectorAll(action.selector);
|
||||
matches.forEach((match) => {
|
||||
const htmlMatch = match as HTMLElement;
|
||||
if (!actionSetRef.current.has(htmlMatch)) {
|
||||
actionSetRef.current.set(htmlMatch, action.type);
|
||||
addedSomething = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of mutation.removedNodes) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const el = node as HTMLElement;
|
||||
|
||||
if (actionSetRef.current.has(el)) {
|
||||
actionSetRef.current.delete(el);
|
||||
removedSomething = true;
|
||||
}
|
||||
|
||||
const action = { selector: provider.element, type: 'post' };
|
||||
if (el.querySelectorAll) {
|
||||
const matches = el.querySelectorAll(action.selector);
|
||||
matches.forEach((match) => {
|
||||
const htmlMatch = match as HTMLElement;
|
||||
if (actionSetRef.current.has(htmlMatch)) {
|
||||
actionSetRef.current.delete(htmlMatch);
|
||||
removedSomething = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mutation.type === 'attributes') {
|
||||
const el = mutation.target;
|
||||
if (el instanceof HTMLElement) {
|
||||
const action = { selector: provider.element, type: 'post' };
|
||||
const matchesNow = el.matches(action.selector);
|
||||
const wasTracked = actionSetRef.current.has(el);
|
||||
|
||||
if (matchesNow && !wasTracked) {
|
||||
actionSetRef.current.set(el, action.type);
|
||||
addedSomething = true;
|
||||
} else if (!matchesNow && wasTracked) {
|
||||
actionSetRef.current.delete(el);
|
||||
removedSomething = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (addedSomething || removedSomething) {
|
||||
const elements: ActionElement[] = [];
|
||||
actionSetRef.current.forEach((actionType, element) => {
|
||||
elements.push({ element, actionType });
|
||||
});
|
||||
setActionElements(elements);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeOldValue: true,
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return actionElements.map((actionEl, index) => (
|
||||
<Fragment key={index}>
|
||||
{createPortal(
|
||||
<ActionComponent
|
||||
target={actionEl.element}
|
||||
keyIndex={index}
|
||||
actionType={actionEl.actionType}
|
||||
provider={provider}
|
||||
wrap={true}
|
||||
selector={stringToABC(
|
||||
provider.element
|
||||
.split(',')
|
||||
.map((z) => z.trim())
|
||||
.find((p) => actionEl.element.matches(p)) || ''
|
||||
)}
|
||||
/>,
|
||||
actionEl.element
|
||||
)}
|
||||
</Fragment>
|
||||
));
|
||||
};
|
||||
|
||||
function stringToABC(text: string, length = 8) {
|
||||
// Simple DJB2-like hash (non-cryptographic!)
|
||||
let hash = 5381;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
hash = (hash * 33) ^ text.charCodeAt(i);
|
||||
}
|
||||
|
||||
hash = Math.abs(hash);
|
||||
|
||||
// Convert to base-26 string using a–z
|
||||
const alphabet = 'abcdefghijklmnopqrstuvwxyz';
|
||||
let result = '';
|
||||
while (result.length < length) {
|
||||
result = alphabet[hash % 26] + result;
|
||||
hash = Math.floor(hash / 26);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
27
apps/extension/src/pages/content/style.css
Normal file
27
apps/extension/src/pages/content/style.css
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.my-wrapper {
|
||||
left: 0 !important;
|
||||
top: 0 !important;
|
||||
position: fixed !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
z-index: 999999 !important;
|
||||
display: flex !important;
|
||||
justify-content: center !important;
|
||||
background: rgba(0, 0, 0, 0.5) !important;
|
||||
}
|
||||
|
||||
.my-wrapper > div {
|
||||
background: white !important;
|
||||
width: 600px !important;
|
||||
height: 300px !important;
|
||||
border-radius: 10px !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
justify-items: center !important;
|
||||
margin-top: 100px !important;
|
||||
color: black !important;
|
||||
}
|
||||
8
apps/extension/src/pages/options/Options.css
Normal file
8
apps/extension/src/pages/options/Options.css
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
.container {
|
||||
width: 100%;
|
||||
height: 50vh;
|
||||
font-size: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
6
apps/extension/src/pages/options/Options.tsx
Normal file
6
apps/extension/src/pages/options/Options.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import React from 'react';
|
||||
import '@gitroom/extension/pages/options/Options.css';
|
||||
|
||||
export default function Options() {
|
||||
return <div className="container">Options</div>;
|
||||
}
|
||||
0
apps/extension/src/pages/options/index.css
Normal file
0
apps/extension/src/pages/options/index.css
Normal file
12
apps/extension/src/pages/options/index.html
Normal file
12
apps/extension/src/pages/options/index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Options</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="__root"></div>
|
||||
<script type="module" src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
13
apps/extension/src/pages/options/index.tsx
Normal file
13
apps/extension/src/pages/options/index.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import '@gitroom/extension/pages/options/index.css';
|
||||
import Options from '@gitroom/extension/pages/options/Options';
|
||||
|
||||
function init() {
|
||||
const rootContainer = document.querySelector('#__root');
|
||||
if (!rootContainer) throw new Error("Can't find Options root element");
|
||||
const root = createRoot(rootContainer);
|
||||
root.render(<Options />);
|
||||
}
|
||||
|
||||
init();
|
||||
7
apps/extension/src/pages/panel/Panel.css
Normal file
7
apps/extension/src/pages/panel/Panel.css
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
body {
|
||||
background-color: #242424;
|
||||
}
|
||||
|
||||
.container {
|
||||
color: #ffffff;
|
||||
}
|
||||
10
apps/extension/src/pages/panel/Panel.tsx
Normal file
10
apps/extension/src/pages/panel/Panel.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import '@pages/panel/Panel.css';
|
||||
|
||||
export default function Panel() {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>Side Panel</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
0
apps/extension/src/pages/panel/index.css
Normal file
0
apps/extension/src/pages/panel/index.css
Normal file
12
apps/extension/src/pages/panel/index.html
Normal file
12
apps/extension/src/pages/panel/index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Devtools Panel</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="__root"></div>
|
||||
<script type="module" src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue