Compare commits
486 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09088a5391 | ||
|
|
faeb89853b | ||
|
|
6fc51da7e3 | ||
|
|
415c9c4ba8 | ||
|
|
e19c855da6 | ||
|
|
7e0bb7075e | ||
|
|
0b3328daeb | ||
|
|
4811741e63 | ||
|
|
7dda2812d7 | ||
|
|
2316a45388 | ||
|
|
38b0ac8c70 | ||
|
|
17fa64726c | ||
|
|
03ddef66e2 | ||
|
|
0dce16029e | ||
|
|
03aa6b13dd | ||
|
|
630602858e | ||
|
|
715d3e40fd | ||
|
|
e63d6d2cf2 | ||
|
|
f2ebadab9e | ||
|
|
1677714670 | ||
|
|
b4635f026b | ||
|
|
5f2f5581b2 | ||
|
|
7cc3d9bd78 | ||
|
|
d2c1eabc8b | ||
|
|
4ee5231cb2 | ||
|
|
16abf0dc9a | ||
|
|
86368d7b7b | ||
|
|
e986d9e493 | ||
|
|
aa0c16b648 | ||
|
|
510f396389 | ||
|
|
80b6bdcabe | ||
|
|
e153ab0a9b | ||
|
|
cf0ab36a23 | ||
|
|
009bd36528 | ||
|
|
905392513f | ||
|
|
7be292094a | ||
|
|
6e55eb3b92 | ||
|
|
39f2a176e1 | ||
|
|
638b071283 | ||
|
|
060c77a68c | ||
|
|
d4405906bd | ||
|
|
c8f1074f48 | ||
|
|
dcb1b0188a | ||
|
|
22f436e72e | ||
|
|
53f0967e67 | ||
|
|
18a1a80871 | ||
|
|
a6967c8519 | ||
|
|
7e92764ad2 | ||
|
|
1bf32426c7 | ||
|
|
779764aa5d | ||
|
|
232ebb2528 | ||
|
|
d056225053 | ||
|
|
e419e05f09 | ||
|
|
47ce014204 | ||
|
|
9d14b0262d | ||
|
|
971042a074 | ||
|
|
c3976e554f | ||
|
|
ef111eb1c4 | ||
|
|
3ee35a7348 | ||
|
|
d6bc6eb0ff | ||
|
|
0d98fc02fb | ||
|
|
7264c00298 | ||
|
|
bb7cd46a4f | ||
|
|
7236213ea4 | ||
|
|
4bdcfec3d7 | ||
|
|
cdcf63bf6b | ||
|
|
fd6553196b | ||
|
|
90b2581048 | ||
|
|
4e7864c929 | ||
|
|
b91ffdc9c3 | ||
|
|
d75662b56a | ||
|
|
876be6f8c6 | ||
|
|
071143dcb0 | ||
|
|
da448012dd | ||
|
|
e51cae1614 | ||
|
|
fa5d7f4c40 | ||
|
|
0b554e6844 | ||
|
|
8951289426 | ||
|
|
ec4759e934 | ||
|
|
c61e061145 | ||
|
|
8cfb634b66 | ||
|
|
55a542485a | ||
|
|
0eddfb3304 | ||
|
|
88006a7614 | ||
|
|
45e55c545d | ||
|
|
0a8fa5bff6 | ||
|
|
65d23707ab | ||
|
|
846954f059 | ||
|
|
c79965718f | ||
|
|
027c9caa96 | ||
|
|
8a7e8eb8f3 | ||
|
|
71b2e2e793 | ||
|
|
2ae293916b | ||
|
|
e5947034ab | ||
|
|
0ecca5298d | ||
|
|
7d9b99abf3 | ||
|
|
45bdf128e9 | ||
|
|
4e277ed32d | ||
|
|
5257f2fabe | ||
|
|
386fc7b049 | ||
|
|
ec8c0f6fb9 | ||
|
|
eb0334d96d | ||
|
|
288a4d428b | ||
|
|
1145e51ea6 | ||
|
|
3ea302202d | ||
|
|
e3b3b82fae | ||
|
|
318e9da8d1 | ||
|
|
186d3370e9 | ||
|
|
98c32c3da5 | ||
|
|
3e8a0ab817 | ||
|
|
6a06b210c2 | ||
|
|
30e8b77709 | ||
|
|
59e535e258 | ||
|
|
a8918a6284 | ||
|
|
26d7ffa0ed | ||
|
|
c7867ab05e | ||
|
|
9b50a18f2d | ||
|
|
943293dc7d | ||
|
|
61a563a06c | ||
|
|
76676510f2 | ||
|
|
af8d04002c | ||
|
|
d16945a9e2 | ||
|
|
fb43544bb2 | ||
|
|
008deb0daf | ||
|
|
c5882d3eae | ||
|
|
1bbfa8d603 | ||
|
|
6bf80ff846 | ||
|
|
f55cca519c | ||
|
|
ac109bf564 | ||
|
|
5d46a6cd34 | ||
|
|
507a006b9f | ||
|
|
073479bc4a | ||
|
|
687925bcbf | ||
|
|
2d434fe198 | ||
|
|
e97be93dc4 | ||
|
|
bb63a7c8a9 | ||
|
|
0f4db4a375 | ||
|
|
3a376f4a9c | ||
|
|
afda06bb6a | ||
|
|
1840a6db6d | ||
|
|
b0832740d0 | ||
|
|
40d9d11f72 | ||
|
|
7b67b4a6b5 | ||
|
|
8c03078086 | ||
|
|
5ae4c950db | ||
|
|
6f7a80f689 | ||
|
|
52f59bcfde | ||
|
|
13fedeca8b | ||
|
|
f55253d1ab | ||
|
|
e6a019b778 | ||
|
|
8bb1935834 | ||
|
|
117dab594e | ||
|
|
777ca3d329 | ||
|
|
666de6ac06 | ||
|
|
2786fbaeba | ||
|
|
6aa5bf3591 | ||
|
|
d8386efc52 | ||
|
|
be5d871896 | ||
|
|
0ad89ccd26 | ||
|
|
92b82837e9 | ||
|
|
0c7000276d | ||
|
|
a8c8428ae2 | ||
|
|
f675dd5179 | ||
|
|
358bf62804 | ||
|
|
e621f965b5 | ||
|
|
072c6bac4a | ||
|
|
c7a88b98bc | ||
|
|
3bc3d004fa | ||
|
|
afe06768fe | ||
|
|
41d48a81fd | ||
|
|
b71297db57 | ||
|
|
c280139917 | ||
|
|
20053fc2f8 | ||
|
|
1aa47351a4 | ||
|
|
40c443ba96 | ||
|
|
cb8560e183 | ||
|
|
9df6ab6b48 | ||
|
|
9a29793e2c | ||
|
|
8d25914bd1 | ||
|
|
ac61be6454 | ||
|
|
8bbed0698d | ||
|
|
da995de88f | ||
|
|
1c04544c54 | ||
|
|
c539fa8cf4 | ||
|
|
c164db76d9 | ||
|
|
8dcfffff53 | ||
|
|
b6d6b7283f | ||
|
|
33f3e552cb | ||
|
|
2f85198887 | ||
|
|
2cd2911de2 | ||
|
|
65939d3341 | ||
|
|
eb14bd9f05 | ||
|
|
fa27e38ca0 | ||
|
|
2f6896b332 | ||
|
|
472ef851c2 | ||
|
|
cf2a980dd8 | ||
|
|
2ae98a925a | ||
|
|
e20565fb23 | ||
|
|
3b79a74092 | ||
|
|
08c3143647 | ||
|
|
41fcb08834 | ||
|
|
3d09e9f2c5 | ||
|
|
f624321b43 | ||
|
|
f1a32f243e | ||
|
|
634bee898a | ||
|
|
5e386155f9 | ||
|
|
983286005a | ||
|
|
3cf1cd05b3 | ||
|
|
01fd2df995 | ||
|
|
0b7fd01a82 | ||
|
|
72e63a41c2 | ||
|
|
2a194b680e | ||
|
|
9fa6786754 | ||
|
|
624a4a9775 | ||
|
|
4ba1c5e415 | ||
|
|
748e2190e9 | ||
|
|
78b834d18f | ||
|
|
ad0b0259eb | ||
|
|
fe51c788c7 | ||
|
|
24a37a56a7 | ||
|
|
5d4808a07a | ||
|
|
cdeef9b4d6 | ||
|
|
778d255a15 | ||
|
|
52d84a2bfb | ||
|
|
42d7927d1c | ||
|
|
354b206280 | ||
|
|
0d7e54023c | ||
|
|
681e6d4cb4 | ||
|
|
09341d345f | ||
|
|
c36a5d54f2 | ||
|
|
11e32c20fb | ||
|
|
72b0dfaf8a | ||
|
|
e4c4d6470c | ||
|
|
334dda7609 | ||
|
|
aa37ad0a11 | ||
|
|
ff4ee6c5fe | ||
|
|
98d9ddc83e | ||
|
|
724e5bc497 | ||
|
|
16713f03b6 | ||
|
|
daa1e0f91b | ||
|
|
5751a62f44 | ||
|
|
d0a6ee330d | ||
|
|
6c39e810db | ||
|
|
71833f3f39 | ||
|
|
92abb07712 | ||
|
|
ff89a21c98 | ||
|
|
7bfa3e5ad9 | ||
|
|
258378770e | ||
|
|
bd82f3e4ca | ||
|
|
292f96f959 | ||
|
|
35f75e7ff5 | ||
|
|
fdd7168742 | ||
|
|
39dd1d4cdd | ||
|
|
7627b80bfc | ||
|
|
d7322d11fd | ||
|
|
b9f3b646fc | ||
|
|
5fddba6e98 | ||
|
|
ccd665715c | ||
|
|
b23298904a | ||
|
|
143bc229de | ||
|
|
001c70357c | ||
|
|
5d1988c2f1 | ||
|
|
118060bacd | ||
|
|
b744b60be6 | ||
|
|
dfb5c05979 | ||
|
|
13104962c6 | ||
|
|
f7f5899d01 | ||
|
|
b854430f7d | ||
|
|
009f2df074 | ||
|
|
613a4285ff | ||
|
|
cfa52b2336 | ||
|
|
381f9e8021 | ||
|
|
16946b1c0f | ||
|
|
9a7e051f5d | ||
|
|
6f54180768 | ||
|
|
38b1cb2ce5 | ||
|
|
ecb1b55ce1 | ||
|
|
7ee7eacbcb | ||
|
|
56efd49d8f | ||
|
|
844449b378 | ||
|
|
c908813d0f | ||
|
|
dc8a380176 | ||
|
|
6a69ac7eb4 | ||
|
|
af08ae1187 | ||
|
|
6aa6dfb614 | ||
|
|
2feadf4673 | ||
|
|
2c475d1238 | ||
|
|
33c8adbef1 | ||
|
|
0c02554064 | ||
|
|
683d4c3682 | ||
|
|
ba71bbf5a8 | ||
|
|
80d8804f5a | ||
|
|
a098d612af | ||
|
|
3b57679430 | ||
|
|
d929c96792 | ||
|
|
7f7c336939 | ||
|
|
d055e69335 | ||
|
|
95956fe345 | ||
|
|
f7d36b36e7 | ||
|
|
82c8b53236 | ||
|
|
0ceaa3d04d | ||
|
|
5869300e64 | ||
|
|
b3681d2ac1 | ||
|
|
945dc09f48 | ||
|
|
029ddb29d1 | ||
|
|
5f5d746f8a | ||
|
|
f5ff72b706 | ||
|
|
e3c3854840 | ||
|
|
f72d63bd43 | ||
|
|
1c3cb008c6 | ||
|
|
ee8b26f383 | ||
|
|
52ec091400 | ||
|
|
9de380f6ba | ||
|
|
c481add80b | ||
|
|
572de8a2d0 | ||
|
|
eccc767701 | ||
|
|
089b9ebc40 | ||
|
|
650d92bb56 | ||
|
|
58bc35148c | ||
|
|
5a193fd19e | ||
|
|
0928f76284 | ||
|
|
c970bfab95 | ||
|
|
34243312ad | ||
|
|
70be520c0b | ||
|
|
e7999ea151 | ||
|
|
979b81d6d0 | ||
|
|
6d173ab7cd | ||
|
|
638debdaa4 | ||
|
|
f5ef80546e | ||
|
|
bb9e731174 | ||
|
|
656f29eb34 | ||
|
|
437a3b5143 | ||
|
|
47414acf29 | ||
|
|
68705fca69 | ||
|
|
a936da4851 | ||
|
|
1a8f6ff6ca | ||
|
|
9b712bb634 | ||
|
|
fe457573ec | ||
|
|
5f830f11b1 | ||
|
|
e1c51effad | ||
|
|
980ea7c5b4 | ||
|
|
42529ac438 | ||
|
|
4ed1ffe206 | ||
|
|
ccce5d8122 | ||
|
|
7909228271 | ||
|
|
17bde5858c | ||
|
|
5c50e962ae | ||
|
|
ee012fb021 | ||
|
|
e551d151d6 | ||
|
|
881c37ed6f | ||
|
|
c3cdee5c21 | ||
|
|
aa17498a52 | ||
|
|
ddbfd16030 | ||
|
|
b6928138d2 | ||
|
|
078d4739c8 | ||
|
|
ec4b19c741 | ||
|
|
2813ed01f9 | ||
|
|
defd9e0a63 | ||
|
|
8c4e69dd3d | ||
|
|
7e92a767e4 | ||
|
|
1d9af510e5 | ||
|
|
f39ab5dc80 | ||
|
|
ec5210be51 | ||
|
|
e9f2660002 | ||
|
|
7e1e5f36e3 | ||
|
|
bf9ab0fd95 | ||
|
|
8bb1fc826f | ||
|
|
608185fd9a | ||
|
|
43ced2e7b6 | ||
|
|
ba7fc7a8e8 | ||
|
|
5cb69667b3 | ||
|
|
baea8b28ee | ||
|
|
9e54e315fd | ||
|
|
98a6fb1013 | ||
|
|
2346223a04 | ||
|
|
1e19868b36 | ||
|
|
5cca81e002 | ||
|
|
bfa552401e | ||
|
|
99889d59e2 | ||
|
|
c8f6cdb643 | ||
|
|
5bad88daa9 | ||
|
|
d8b8b3d629 | ||
|
|
8a577dea73 | ||
|
|
809476aa1d | ||
|
|
0d5b45cd11 | ||
|
|
604b029a9d | ||
|
|
87eca91b9b | ||
|
|
88e05853c1 | ||
|
|
34b465aaa1 | ||
|
|
005e3f753b | ||
|
|
e29812501e | ||
|
|
7eeb1cb044 | ||
|
|
5bfb97e9a4 | ||
|
|
ba0a2498a4 | ||
|
|
47e7c843c9 | ||
|
|
ba4ad5deb2 | ||
|
|
07b0c2e85d | ||
|
|
13d4bb0086 | ||
|
|
abd73039c3 | ||
|
|
0af727e0cf | ||
|
|
563e7a257b | ||
|
|
d7a92e0b92 | ||
|
|
5665774260 | ||
|
|
6859f0d049 | ||
|
|
82d91fd8ed | ||
|
|
b2677649df | ||
|
|
fddf61f052 | ||
|
|
05b6b2ec00 | ||
|
|
fca16b5c48 | ||
|
|
31ccf8d352 | ||
|
|
ce94f9bcc4 | ||
|
|
c5bacf7d43 | ||
|
|
38cbeb8d95 | ||
|
|
46b1263304 | ||
|
|
631d7c15e8 | ||
|
|
3e8f5ddca6 | ||
|
|
07238a06a6 | ||
|
|
96661cc30d | ||
|
|
774d29d798 | ||
|
|
bd24282d2b | ||
|
|
82a92afabb | ||
|
|
2cc639bbfc | ||
|
|
2544e870aa | ||
|
|
4836b2e7c1 | ||
|
|
407cfa67ef | ||
|
|
7e73017d3f | ||
|
|
bddb937258 | ||
|
|
7001295fa5 | ||
|
|
475cf8e0fb | ||
|
|
0f4c39ede0 | ||
|
|
012a347ed6 | ||
|
|
633c08fde7 | ||
|
|
8b0fe19b41 | ||
|
|
95eace849b | ||
|
|
5f1a77a3b0 | ||
|
|
7ac99e8259 | ||
|
|
5d76475801 | ||
|
|
bb9aa1aee6 | ||
|
|
ead98cb41f | ||
|
|
fc349942b5 | ||
|
|
c7b83ee361 | ||
|
|
3f674de401 | ||
|
|
de74191bab | ||
|
|
691d15db03 | ||
|
|
dac6e8e189 | ||
|
|
204739f049 | ||
|
|
0144e39841 | ||
|
|
1291937cb2 | ||
|
|
b5979b290d | ||
|
|
6eb55e128f | ||
|
|
78e1a14388 | ||
|
|
c740da360b | ||
|
|
95303b2975 | ||
|
|
6633fab924 | ||
|
|
da0045428a | ||
|
|
d8a6215155 | ||
|
|
9e0eff7e5a | ||
|
|
cd085e27ca | ||
|
|
b3d298daff | ||
|
|
4f0f4c27f9 | ||
|
|
c8c812e1eb | ||
|
|
44f190d466 | ||
|
|
5f4de7c987 | ||
|
|
faef57738c | ||
|
|
0e5d6f8826 | ||
|
|
6729de4563 | ||
|
|
ca941aed85 | ||
|
|
00caa7ddc4 | ||
|
|
e5a4c558dd | ||
|
|
b9062423a4 | ||
|
|
7a82798cdc | ||
|
|
3387b3c989 | ||
|
|
e0b496c4f9 | ||
|
|
5f945bd396 | ||
|
|
a188baa6f7 | ||
|
|
9c751fb725 | ||
|
|
8c1191a093 | ||
|
|
2cf0d632de | ||
|
|
0d134c0a6f | ||
|
|
90da9d4aff | ||
|
|
6f889d42c8 | ||
|
|
f98ae083ab | ||
|
|
1c61e76c9f | ||
|
|
70d07249d3 | ||
|
|
13137e803d | ||
|
|
1da332eec1 |
555 changed files with 41699 additions and 21566 deletions
|
|
@ -40,6 +40,7 @@ STORAGE_PROVIDER="local"
|
|||
#NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY=""
|
||||
|
||||
# Social Media API Settings
|
||||
X_URL=""
|
||||
X_API_KEY=""
|
||||
X_API_SECRET=""
|
||||
LINKEDIN_CLIENT_ID=""
|
||||
|
|
@ -76,6 +77,9 @@ 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=""
|
||||
|
|
|
|||
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -1,3 +1,5 @@
|
|||
<!-- 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, ...
|
||||
|
|
@ -15,5 +17,6 @@ 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 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.
|
||||
- [ ] 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
|
||||
|
|
|
|||
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
|
|
@ -1,11 +0,0 @@
|
|||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
BIN
.github/sponsors/hostinger.png
vendored
Normal file
BIN
.github/sponsors/hostinger.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
24
.github/workflows/build.yml
vendored
24
.github/workflows/build.yml
vendored
|
|
@ -3,6 +3,8 @@ name: Build
|
|||
|
||||
on:
|
||||
push:
|
||||
merge_group:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
@ -11,7 +13,7 @@ jobs:
|
|||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: ['20.17.0']
|
||||
node-version: ['22.12.0']
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
|
|
@ -27,7 +29,7 @@ jobs:
|
|||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
|
|
@ -35,15 +37,15 @@ jobs:
|
|||
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: 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
|
||||
|
|
|
|||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
name: "Code Quality Analysis"
|
||||
name: "Code Quality Analysis"
|
||||
|
||||
on:
|
||||
push:
|
||||
|
|
@ -9,6 +9,8 @@ on:
|
|||
- apps/**
|
||||
- '!apps/docs/**'
|
||||
- libraries/**
|
||||
merge_group:
|
||||
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
|
|
|
|||
38
.github/workflows/pr-docker-build.yml
vendored
38
.github/workflows/pr-docker-build.yml
vendored
|
|
@ -1,38 +0,0 @@
|
|||
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,3 +58,6 @@ 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
Normal file
61
CLAUDE.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
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,6 +6,10 @@ 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:
|
||||
|
|
@ -27,6 +31,11 @@ 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,11 +1,18 @@
|
|||
FROM node:22.20-alpine
|
||||
FROM node:22.20-bookworm-slim
|
||||
ARG NEXT_PUBLIC_VERSION
|
||||
ENV NEXT_PUBLIC_VERSION=$NEXT_PUBLIC_VERSION
|
||||
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 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 npm --no-update-notifier --no-fund --global install pnpm@10.6.1 pm2
|
||||
|
|
|
|||
23
README.md
23
README.md
|
|
@ -13,6 +13,7 @@
|
|||
</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 />
|
||||
|
|
@ -64,11 +65,14 @@
|
|||
<a href="https://apps.make.com/postiz">Make.com integration</a>
|
||||
</p>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<br />
|
||||
## 🔌 See the leading Postiz features
|
||||
|
||||
<p align="center">
|
||||
<video src="https://github.com/user-attachments/assets/05436a01-19c8-4827-b57f-05a5e7637a67" width="100%" />
|
||||
<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>
|
||||
</p>
|
||||
|
||||
## ✨ Features
|
||||
|
|
@ -77,6 +81,15 @@
|
|||
| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
|  |  |
|
||||
|
||||
### 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)
|
||||
|
|
@ -88,11 +101,11 @@
|
|||
|
||||
## Tech Stack
|
||||
|
||||
- NX (Monorepo)
|
||||
- Pnpm workspaces (Monorepo)
|
||||
- NextJS (React)
|
||||
- NestJS
|
||||
- Prisma (Default to PostgreSQL)
|
||||
- Redis (BullMQ)
|
||||
- Temporal
|
||||
- Resend (email notifications)
|
||||
|
||||
## Quick Start
|
||||
|
|
@ -119,7 +132,7 @@ Link: https://opencollective.com/postiz
|
|||
|
||||
## Star History
|
||||
|
||||
[](https://www.star-history.com/#gitroomhq/postiz-app&Date)
|
||||
[](https://www.star-history.com/#gitroomhq/postiz-app&type=date&legend=top-left)
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
52
SECURITY.md
52
SECURITY.md
|
|
@ -4,26 +4,48 @@
|
|||
|
||||
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.
|
||||
|
||||
## Reporting Security Vulnerabilities
|
||||
## Scope
|
||||
|
||||
If you discover a security vulnerability in the Postiz app, please report it to us privately via email to one of the maintainers:
|
||||
We, at Postiz (gitroomhq), cover the following scopes for vulnerability disclosures:
|
||||
|
||||
- @nevo-david
|
||||
- @ennogelhaus ([email](mailto:gelhausenno@outlook.de))
|
||||
- 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
|
||||
|
||||
When reporting a security vulnerability, please provide as much detail as possible, including:
|
||||
|
||||
- A clear description of the vulnerability
|
||||
- Steps to reproduce the vulnerability
|
||||
- Any relevant code or configuration files
|
||||
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).
|
||||
|
||||
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:
|
||||
|
||||
- @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.
|
||||
|
||||
## Disclosure Guidelines
|
||||
|
||||
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 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 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.
|
||||
|
||||
|
|
@ -36,8 +58,12 @@ 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.
|
||||
|
||||
## Template Attribution
|
||||
## Response 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).
|
||||
We aim to follow these timelines:
|
||||
|
||||
Thank you for helping to keep the `postiz-app` secure!
|
||||
- **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.
|
||||
|
|
@ -16,13 +16,10 @@ 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';
|
||||
|
|
@ -34,6 +31,19 @@ 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,
|
||||
|
|
@ -44,15 +54,17 @@ const authenticatedController = [
|
|||
MediaController,
|
||||
BillingController,
|
||||
NotificationsController,
|
||||
MarketplaceController,
|
||||
MessagesController,
|
||||
CopilotController,
|
||||
AgenciesController,
|
||||
WebhookController,
|
||||
SignatureController,
|
||||
AutopostController,
|
||||
SetsController,
|
||||
ThirdPartyController,
|
||||
OAuthAppController,
|
||||
ApprovedAppsController,
|
||||
OAuthAuthorizedController,
|
||||
AnnouncementsController,
|
||||
AdminController,
|
||||
];
|
||||
@Module({
|
||||
imports: [UploadModule],
|
||||
|
|
@ -62,6 +74,9 @@ const authenticatedController = [
|
|||
AuthController,
|
||||
PublicController,
|
||||
MonitorController,
|
||||
EnterpriseController,
|
||||
NoAuthIntegrationsController,
|
||||
OAuthController,
|
||||
...authenticatedController,
|
||||
],
|
||||
providers: [
|
||||
|
|
@ -77,6 +92,12 @@ const authenticatedController = [
|
|||
TrackService,
|
||||
ShortLinkService,
|
||||
Nowpayments,
|
||||
AuthProviderManager,
|
||||
GithubProvider,
|
||||
GoogleProvider,
|
||||
FarcasterProvider,
|
||||
WalletProvider,
|
||||
OauthProvider,
|
||||
],
|
||||
get exports() {
|
||||
return [...this.imports, ...this.providers];
|
||||
|
|
|
|||
47
apps/backend/src/api/routes/admin.controller.ts
Normal file
47
apps/backend/src/api/routes/admin.controller.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
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 getAgencyByUser(@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,56 +1,17 @@
|
|||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Inject,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { Controller, Get, Param, 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 { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
|
||||
@ApiTags('Analytics')
|
||||
@Controller('/analytics')
|
||||
export class AnalyticsController {
|
||||
constructor(
|
||||
private _starsService: StarsService,
|
||||
private _integrationService: IntegrationService
|
||||
private _integrationService: IntegrationService,
|
||||
private _postsService: PostsService
|
||||
) {}
|
||||
@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(
|
||||
|
|
@ -60,4 +21,13 @@ 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
47
apps/backend/src/api/routes/announcements.controller.ts
Normal file
47
apps/backend/src/api/routes/announcements.controller.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
24
apps/backend/src/api/routes/approved-apps.controller.ts
Normal file
24
apps/backend/src/api/routes/approved-apps.controller.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
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,6 +15,7 @@ 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';
|
||||
|
|
@ -102,7 +103,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,
|
||||
|
|
@ -198,6 +199,19 @@ 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);
|
||||
|
|
@ -206,9 +220,13 @@ 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);
|
||||
const activate = await this._authService.activate(
|
||||
code,
|
||||
datafast_visitor_id
|
||||
);
|
||||
if (!activate) {
|
||||
return response.status(200).json({ can: false });
|
||||
}
|
||||
|
|
@ -234,13 +252,33 @@ 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);
|
||||
const { jwt, token } = await this._authService.checkExists(
|
||||
provider,
|
||||
code,
|
||||
redirect_uri
|
||||
);
|
||||
|
||||
if (token) {
|
||||
return response.json({ token });
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ 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')
|
||||
|
|
@ -62,7 +63,7 @@ export class AutopostController {
|
|||
}
|
||||
|
||||
@Post('/send')
|
||||
async sendWebhook(@Query('url') url: string) {
|
||||
return this._autopostsService.loadXML(url);
|
||||
async sendWebhook(@Query() query: OnlyURL) {
|
||||
return this._autopostsService.loadXML(query.url);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Body, Controller, Get, Param, Post, Req } from '@nestjs/common';
|
||||
import { Body, Controller, Get, HttpException, 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';
|
||||
|
|
@ -144,6 +144,43 @@ 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 { RuntimeContext } from '@mastra/core/di';
|
||||
import { RequestContext } 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,20 +72,19 @@ export class CopilotController {
|
|||
return;
|
||||
}
|
||||
const mastra = await this._mastraService.mastra();
|
||||
const runtimeContext = new RuntimeContext<ChannelsContext>();
|
||||
runtimeContext.set(
|
||||
const requestContext = new RequestContext<ChannelsContext>();
|
||||
requestContext.set(
|
||||
'integrations',
|
||||
req?.body?.variables?.properties?.integrations || []
|
||||
);
|
||||
|
||||
runtimeContext.set('organization', JSON.stringify(organization));
|
||||
runtimeContext.set('ui', 'true');
|
||||
requestContext.set('organization', JSON.stringify(organization));
|
||||
requestContext.set('ui', 'true');
|
||||
|
||||
const agents = MastraAgent.getLocalAgents({
|
||||
resourceId: organization.id,
|
||||
mastra,
|
||||
// @ts-ignore
|
||||
runtimeContext,
|
||||
requestContext: requestContext as any,
|
||||
});
|
||||
|
||||
const runtime = new CopilotRuntime({
|
||||
|
|
@ -124,7 +123,7 @@ export class CopilotController {
|
|||
const mastra = await this._mastraService.mastra();
|
||||
const memory = await mastra.getAgent('postiz').getMemory();
|
||||
try {
|
||||
return await memory.query({
|
||||
return await memory.recall({
|
||||
resourceId: organization.id,
|
||||
threadId,
|
||||
});
|
||||
|
|
@ -137,14 +136,12 @@ 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.getThreadsByResourceIdPaginated({
|
||||
resourceId: organization.id,
|
||||
const list = await memory.listThreads({
|
||||
filter: { resourceId: organization.id },
|
||||
perPage: 100000,
|
||||
page: 0,
|
||||
orderBy: 'createdAt',
|
||||
sortDirection: 'DESC',
|
||||
orderBy: { field: 'createdAt', direction: 'DESC' },
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
|||
128
apps/backend/src/api/routes/enterprise.controller.ts
Normal file
128
apps/backend/src/api/routes/enterprise.controller.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
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,15 +3,12 @@ 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';
|
||||
|
|
@ -21,18 +18,14 @@ 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 {
|
||||
NotEnoughScopes,
|
||||
RefreshToken,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { 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,
|
||||
|
|
@ -49,9 +42,15 @@ export class IntegrationsController {
|
|||
private _postService: PostsService,
|
||||
private _refreshIntegrationService: RefreshIntegrationService
|
||||
) {}
|
||||
@Get('/')
|
||||
getIntegrations() {
|
||||
return this._integrationManager.getAllIntegrations();
|
||||
|
||||
@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('/:identifier/internal-plugs')
|
||||
|
|
@ -102,6 +101,7 @@ 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,7 +195,10 @@ export class IntegrationsController {
|
|||
async getIntegrationUrl(
|
||||
@Param('integration') integration: string,
|
||||
@Query('refresh') refresh: string,
|
||||
@Query('externalUrl') externalUrl: string
|
||||
@Query('externalUrl') externalUrl: string,
|
||||
@Query('redirectUrl') redirectUrl: string,
|
||||
@Query('onboarding') onboarding: string,
|
||||
@GetOrgFromRequest() org: Organization
|
||||
) {
|
||||
if (
|
||||
!this._integrationManager
|
||||
|
|
@ -224,15 +227,24 @@ export class IntegrationsController {
|
|||
await integrationProvider.generateAuthUrl(getExternalUrl);
|
||||
|
||||
if (refresh) {
|
||||
await ioRedis.set(`refresh:${state}`, refresh, 'EX', 300);
|
||||
await ioRedis.set(`refresh:${state}`, refresh, 'EX', 3600);
|
||||
}
|
||||
|
||||
await ioRedis.set(`login:${state}`, codeVerifier, '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(
|
||||
`external:${state}`,
|
||||
JSON.stringify(getExternalUrl),
|
||||
'EX',
|
||||
300
|
||||
3600
|
||||
);
|
||||
|
||||
return { url };
|
||||
|
|
@ -366,154 +378,6 @@ 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, refreshToken: body.refresh });
|
||||
}
|
||||
|
||||
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,
|
||||
|
|
@ -522,15 +386,6 @@ export class IntegrationsController {
|
|||
return this._integrationService.disableChannel(org.id, id);
|
||||
}
|
||||
|
||||
@Post('/provider/:id/connect')
|
||||
async saveProviderPage(
|
||||
@Param('id') id: string,
|
||||
@Body() body: any,
|
||||
@GetOrgFromRequest() org: Organization
|
||||
) {
|
||||
return this._integrationService.saveProviderPage(org.id, id, body);
|
||||
}
|
||||
|
||||
@Post('/enable')
|
||||
enableChannel(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
|
|
@ -555,7 +410,7 @@ export class IntegrationsController {
|
|||
);
|
||||
if (isTherePosts.length) {
|
||||
for (const post of isTherePosts) {
|
||||
await this._postService.deletePost(org.id, post.group);
|
||||
this._postService.deletePost(org.id, post.group).catch((err) => {});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -597,4 +452,30 @@ 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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,242 +0,0 @@
|
|||
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,11 +91,13 @@ 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
|
||||
uploadedFile.path,
|
||||
originalName
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -103,7 +105,8 @@ export class MediaController {
|
|||
async saveMedia(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Req() req: Request,
|
||||
@Body('name') name: string
|
||||
@Body('name') name: string,
|
||||
@Body('originalName') originalName: string
|
||||
) {
|
||||
if (!name) {
|
||||
return false;
|
||||
|
|
@ -111,7 +114,8 @@ export class MediaController {
|
|||
return this._mediaService.saveFile(
|
||||
org.id,
|
||||
name,
|
||||
process.env.CLOUDFLARE_BUCKET_URL + '/' + name
|
||||
process.env.CLOUDFLARE_BUCKET_URL + '/' + name,
|
||||
originalName || undefined
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -125,11 +129,13 @@ 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') {
|
||||
|
|
@ -140,7 +146,8 @@ export class MediaController {
|
|||
return this._mediaService.saveFile(
|
||||
org.id,
|
||||
getFile.originalname,
|
||||
getFile.path
|
||||
getFile.path,
|
||||
originalName
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -158,12 +165,14 @@ 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
|
||||
upload.Location,
|
||||
originalName || undefined
|
||||
);
|
||||
|
||||
res.status(200).json({ ...upload, saved: saveFile });
|
||||
|
|
@ -172,9 +181,10 @@ export class MediaController {
|
|||
@Get('/')
|
||||
getMedia(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Query('page') page: number
|
||||
@Query('page') page: number,
|
||||
@Query('search') search?: string
|
||||
) {
|
||||
return this._mediaService.getMedia(org.id, page);
|
||||
return this._mediaService.getMedia(org.id, page, search);
|
||||
}
|
||||
|
||||
@Get('/video-options')
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
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,30 +1,14 @@
|
|||
import { Controller, Get, HttpException, Param } from '@nestjs/common';
|
||||
import { Controller, Get, 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) {
|
||||
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
|
||||
);
|
||||
return {
|
||||
status: 'success',
|
||||
message: `Queue ${name} is healthy.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
395
apps/backend/src/api/routes/no.auth.integrations.controller.ts
Normal file
395
apps/backend/src/api/routes/no.auth.integrations.controller.ts
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
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 };
|
||||
}
|
||||
}
|
||||
54
apps/backend/src/api/routes/oauth-app.controller.ts
Normal file
54
apps/backend/src/api/routes/oauth-app.controller.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
95
apps/backend/src/api/routes/oauth.controller.ts
Normal file
95
apps/backend/src/api/routes/oauth.controller.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
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,6 +3,7 @@ import {
|
|||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpException,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
|
|
@ -13,10 +14,9 @@ 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 { StarsService } from '@gitroom/nestjs-libraries/database/prisma/stars/stars.service';
|
||||
import { GetPostsListDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.list.dto';
|
||||
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,15 +24,16 @@ 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
|
||||
) {}
|
||||
|
|
@ -45,17 +46,26 @@ export class PostsController {
|
|||
return this._postsService.getStatistics(org.id, id);
|
||||
}
|
||||
|
||||
@Post('/should-shortlink')
|
||||
async shouldShortlink(@Body() body: { messages: string[] }) {
|
||||
return { ask: this._shortLinkService.askShortLinkedin(body.messages) };
|
||||
}
|
||||
|
||||
@Get('/marketplace/:id')
|
||||
async getMarketplacePosts(
|
||||
@Get('/:id/missing')
|
||||
async getMissingContent(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
return this._messagesService.getMarketplaceAvailableOffers(org.id, id);
|
||||
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) };
|
||||
}
|
||||
|
||||
@Post('/:id/comments')
|
||||
|
|
@ -90,16 +100,20 @@ 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
|
||||
) {
|
||||
const posts = await this._postsService.getPosts(org.id, query);
|
||||
|
||||
return {
|
||||
posts,
|
||||
};
|
||||
return this._postsService.getPostsMinified(org.id, query);
|
||||
}
|
||||
|
||||
@Get('/find-slot')
|
||||
|
|
@ -115,9 +129,12 @@ export class PostsController {
|
|||
return { date: await this._postsService.findFreeDateTime(org.id, id) };
|
||||
}
|
||||
|
||||
@Get('/predict-trending')
|
||||
predictTrending() {
|
||||
return this._starsService.predictTrending();
|
||||
@Get('/list')
|
||||
async getPostsList(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Query() query: GetPostsListDto
|
||||
) {
|
||||
return this._postsService.getPostsList(org.id, query);
|
||||
}
|
||||
|
||||
@Get('/old')
|
||||
|
|
@ -128,6 +145,23 @@ 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);
|
||||
|
|
@ -141,7 +175,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);
|
||||
return this._postsService.createPost(org.id, body, 'WEB');
|
||||
}
|
||||
|
||||
@Post('/generator/draft')
|
||||
|
|
@ -180,9 +214,10 @@ export class PostsController {
|
|||
changeDate(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string,
|
||||
@Body('date') date: string
|
||||
@Body('date') date: string,
|
||||
@Body('action') action: 'schedule' | 'update' = 'schedule'
|
||||
) {
|
||||
return this._postsService.changeDate(org.id, id, date);
|
||||
return this._postsService.changeDate(org.id, id, date, action);
|
||||
}
|
||||
|
||||
@Post('/separate-posts')
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ 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';
|
||||
|
|
@ -21,8 +20,14 @@ 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);
|
||||
|
||||
|
|
@ -30,11 +35,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 _nowpayments: Nowpayments,
|
||||
private _subscriptionService: SubscriptionService
|
||||
) {}
|
||||
@Post('/agent')
|
||||
async createAgent(@Body() body: { text: string; apiKey: string }) {
|
||||
|
|
@ -48,26 +53,6 @@ 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(
|
||||
|
|
@ -145,6 +130,32 @@ 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);
|
||||
|
|
@ -153,10 +164,11 @@ export class PublicController {
|
|||
|
||||
@Get('/stream')
|
||||
async streamFile(
|
||||
@Query('url') url: string,
|
||||
@Query() query: OnlyURL,
|
||||
@Res() res: Response,
|
||||
@Req() req: Request
|
||||
) {
|
||||
const { url } = query;
|
||||
if (!url.endsWith('mp4')) {
|
||||
return res.status(400).send('Invalid video URL');
|
||||
}
|
||||
|
|
@ -166,7 +178,47 @@ export class PublicController {
|
|||
req.on('aborted', onClose);
|
||||
res.on('close', onClose);
|
||||
|
||||
const r = await fetch(url, { signal: ac.signal });
|
||||
// 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');
|
||||
}
|
||||
|
||||
if (!r.ok && r.status !== 206) {
|
||||
res.status(r.status);
|
||||
|
|
@ -189,7 +241,6 @@ 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,95 +12,9 @@ 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],
|
||||
|
|
@ -133,4 +47,21 @@ 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,47 +1,19 @@
|
|||
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>) {
|
||||
|
|
@ -66,8 +38,6 @@ 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':
|
||||
|
|
@ -81,11 +51,4 @@ 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,6 +14,7 @@ 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')
|
||||
|
|
@ -121,6 +122,52 @@ 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,6 +9,7 @@ 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';
|
||||
|
|
@ -42,6 +43,23 @@ 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,
|
||||
|
|
@ -69,13 +87,14 @@ 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 getPersonal(@GetUserFromRequest() user: User) {
|
||||
async getPersonalInformation(@GetUserFromRequest() user: User) {
|
||||
return this._userService.getPersonal(user.id);
|
||||
}
|
||||
|
||||
|
|
@ -139,6 +158,12 @@ export class UsersController {
|
|||
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,8 +14,7 @@ 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 {
|
||||
UpdateDto,
|
||||
WebhooksDto,
|
||||
OnlyURL, UpdateDto, WebhooksDto
|
||||
} from '@gitroom/nestjs-libraries/dtos/webhooks/webhooks.dto';
|
||||
import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
|
||||
|
|
@ -55,9 +54,9 @@ export class WebhookController {
|
|||
}
|
||||
|
||||
@Post('/send')
|
||||
async sendWebhook(@Body() body: any, @Query('url') url: string) {
|
||||
async sendWebhook(@Body() body: any, @Query() query: OnlyURL) {
|
||||
try {
|
||||
await fetch(url, {
|
||||
await fetch(query.url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ 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';
|
||||
|
|
@ -13,12 +12,16 @@ 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,
|
||||
|
|
@ -26,12 +29,18 @@ import { ChatModule } from '@gitroom/nestjs-libraries/chat/chat.module';
|
|||
ThirdPartyModule,
|
||||
VideoModule,
|
||||
ChatModule,
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: 3600000,
|
||||
limit: process.env.API_LIMIT ? Number(process.env.API_LIMIT) : 30,
|
||||
},
|
||||
]),
|
||||
getTemporalModule(false),
|
||||
TemporalRegisterMissingSearchAttributesModule,
|
||||
InfiniteWorkflowRegisterModule,
|
||||
ThrottlerModule.forRoot({
|
||||
throttlers: [
|
||||
{
|
||||
ttl: 3600000,
|
||||
limit: process.env.API_LIMIT ? Number(process.env.API_LIMIT) : 90,
|
||||
},
|
||||
],
|
||||
storage: new ThrottlerStorageRedisService(ioRedis),
|
||||
}),
|
||||
],
|
||||
controllers: [],
|
||||
providers: [
|
||||
|
|
@ -46,7 +55,6 @@ import { ChatModule } from '@gitroom/nestjs-libraries/chat/chat.module';
|
|||
},
|
||||
],
|
||||
exports: [
|
||||
BullMqModule,
|
||||
DatabaseModule,
|
||||
ApiModule,
|
||||
PublicApiModule,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
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';
|
||||
|
||||
|
|
@ -21,7 +24,14 @@ async function start() {
|
|||
rawBody: true,
|
||||
cors: {
|
||||
...(!process.env.NOT_SECURED ? { credentials: true } : {}),
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'x-copilotkit-runtime-client-gql-version'],
|
||||
allowedHeaders: [
|
||||
'Content-Type',
|
||||
'Authorization',
|
||||
'auth',
|
||||
'showorg',
|
||||
'impersonate',
|
||||
'x-copilotkit-runtime-client-gql-version',
|
||||
],
|
||||
exposedHeaders: [
|
||||
'reload',
|
||||
'onboarding',
|
||||
|
|
@ -45,11 +55,12 @@ async function start() {
|
|||
})
|
||||
);
|
||||
|
||||
app.use('/copilot/*', (req: any, res: any, next: any) => {
|
||||
app.use(['/copilot/{*splat}', '/posts'], (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());
|
||||
|
||||
|
|
@ -59,6 +70,7 @@ 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,10 +6,13 @@ 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';
|
||||
|
|
@ -20,6 +23,7 @@ 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,
|
||||
|
|
@ -27,29 +31,56 @@ 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 axios from 'axios';
|
||||
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
|
||||
import { GetNotificationsDto } from '@gitroom/nestjs-libraries/dtos/notifications/get.notifications.dto';
|
||||
import { Readable } from 'stream';
|
||||
import { lookup } from 'mime-types';
|
||||
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 * 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 _mediaService: MediaService,
|
||||
private _notificationService: NotificationService,
|
||||
private _integrationManager: IntegrationManager,
|
||||
private _refreshIntegrationService: RefreshIntegrationService
|
||||
) {}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
|
@ -67,23 +98,32 @@ export class PublicIntegrationsController {
|
|||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: UploadDto
|
||||
) {
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
const response = await axios.get(body.url, {
|
||||
responseType: 'arraybuffer',
|
||||
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,
|
||||
});
|
||||
|
||||
const buffer = Buffer.from(response.data);
|
||||
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 getFile = await this.storage.uploadFile({
|
||||
buffer,
|
||||
mimetype: lookup(body?.url?.split?.('?')?.[0]) || 'image/jpeg',
|
||||
mimetype,
|
||||
size: buffer.length,
|
||||
path: '',
|
||||
fieldname: '',
|
||||
destination: '',
|
||||
stream: new Readable(),
|
||||
filename: '',
|
||||
originalname: '',
|
||||
originalname: `upload.${ext}`,
|
||||
encoding: '',
|
||||
});
|
||||
|
||||
|
|
@ -99,7 +139,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) };
|
||||
}
|
||||
|
||||
|
|
@ -108,7 +148,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,
|
||||
|
|
@ -122,7 +162,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,
|
||||
|
|
@ -130,29 +170,63 @@ 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);
|
||||
return this._postsService.createPost(org.id, body, creationMethod);
|
||||
}
|
||||
|
||||
@Delete('/posts/:id')
|
||||
async deletePost(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param() body: { id: string }
|
||||
@Param('id') id: string
|
||||
) {
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
const getPostById = await this._postsService.getPost(org.id, body.id);
|
||||
Sentry.metrics.count('public_api-request', 1);
|
||||
const getPostById = await this._postsService.getPost(org.id, 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,
|
||||
|
|
@ -171,22 +245,271 @@ 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 { ProvidersFactory } from '@gitroom/backend/services/auth/providers/providers.factory';
|
||||
import { AuthProviderManager } from '@gitroom/backend/services/auth/providers/providers.manager';
|
||||
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,10 +18,14 @@ export class AuthService {
|
|||
private _userService: UsersService,
|
||||
private _organizationService: OrganizationService,
|
||||
private _notificationService: NotificationService,
|
||||
private _emailService: EmailService
|
||||
private _emailService: EmailService,
|
||||
private _providerManager: AuthProviderManager
|
||||
) {}
|
||||
async canRegister(provider: string) {
|
||||
if (process.env.DISABLE_REGISTRATION !== 'true' || provider === Provider.GENERIC) {
|
||||
if (
|
||||
process.env.DISABLE_REGISTRATION !== 'true' ||
|
||||
provider === Provider.GENERIC
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -39,6 +43,9 @@ 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) {
|
||||
|
|
@ -69,7 +76,8 @@ 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`
|
||||
`Click <a href="${process.env.FRONTEND_URL}/auth/activate/${obj.jwt}">here</a> to activate your account`,
|
||||
'top'
|
||||
);
|
||||
return obj;
|
||||
}
|
||||
|
|
@ -132,7 +140,7 @@ export class AuthService {
|
|||
ip: string,
|
||||
userAgent: string
|
||||
) {
|
||||
const providerInstance = ProvidersFactory.loadProvider(provider);
|
||||
const providerInstance = this._providerManager.getProvider(provider);
|
||||
const providerUser = await providerInstance.getUser(body.providerToken);
|
||||
|
||||
if (!providerUser) {
|
||||
|
|
@ -158,16 +166,54 @@ 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) {
|
||||
|
|
@ -198,7 +244,7 @@ export class AuthService {
|
|||
return this._userService.updatePassword(user.id, body.password);
|
||||
}
|
||||
|
||||
async activate(code: string) {
|
||||
async activate(code: string, tracking: string) {
|
||||
const user = AuthChecker.verifyJWT(code) as {
|
||||
id: string;
|
||||
activated: boolean;
|
||||
|
|
@ -211,6 +257,7 @@ 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);
|
||||
}
|
||||
|
|
@ -218,18 +265,37 @@ export class AuthService {
|
|||
return false;
|
||||
}
|
||||
|
||||
oauthLink(provider: string, query?: any) {
|
||||
const providerInstance = ProvidersFactory.loadProvider(
|
||||
provider as Provider
|
||||
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);
|
||||
return providerInstance.generateLink(query);
|
||||
}
|
||||
|
||||
async checkExists(provider: string, code: string) {
|
||||
const providerInstance = ProvidersFactory.loadProvider(
|
||||
provider as Provider
|
||||
);
|
||||
const token = await providerInstance.getToken(code);
|
||||
async checkExists(provider: string, code: string, redirectUri?: string) {
|
||||
const providerInstance = this._providerManager.getProvider(provider);
|
||||
const token = await providerInstance.getToken(code, redirectUri);
|
||||
const user = await providerInstance.getUser(token);
|
||||
if (!user) {
|
||||
throw new Error('Invalid user');
|
||||
|
|
@ -246,6 +312,9 @@ export class AuthService {
|
|||
}
|
||||
|
||||
private async jwt(user: User) {
|
||||
if (user.password) {
|
||||
delete user.password;
|
||||
}
|
||||
return AuthChecker.signJWT(user);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ export class PoliciesGuard implements CanActivate {
|
|||
const request: Request = context.switchToHttp().getRequest();
|
||||
if (
|
||||
request.path.indexOf('/auth') > -1 ||
|
||||
request.path.indexOf('/stripe') > -1
|
||||
request.path.indexOf('/auth') > -1 ||
|
||||
request.path.indexOf('/integrations/social-connect') > -1 ||
|
||||
request.path.indexOf('/integrations/provider') > -1
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -42,8 +44,10 @@ 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);
|
||||
const ability = await this._authorizationService.check(org.id, org.createdAt, org.users[0].role, policyHandlers, refreshChannelId);
|
||||
|
||||
const item = policyHandlers.find(
|
||||
(handler) => !this.execPolicyHandler(handler, ability)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ export class PermissionsService {
|
|||
orgId: string,
|
||||
created_at: Date,
|
||||
permission: 'USER' | 'ADMIN' | 'SUPERADMIN',
|
||||
requestedPermission: Array<[AuthorizationActions, Sections]>
|
||||
requestedPermission: Array<[AuthorizationActions, Sections]>,
|
||||
refreshChannelId?: string
|
||||
) {
|
||||
const { can, build } = new AbilityBuilder<
|
||||
Ability<[AuthorizationActions, Sections]>
|
||||
|
|
@ -65,6 +66,20 @@ 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,7 +1,34 @@
|
|||
export interface ProvidersInterface {
|
||||
generateLink(query?: any): Promise<string> | string;
|
||||
getToken(code: string): Promise<string>;
|
||||
getUser(
|
||||
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(
|
||||
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,16 +1,20 @@
|
|||
import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.interface';
|
||||
import {
|
||||
AuthProvider,
|
||||
AuthProviderAbstract,
|
||||
} 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',
|
||||
});
|
||||
|
||||
export class FarcasterProvider implements ProvidersInterface {
|
||||
@AuthProvider({ provider: 'FARCASTER' })
|
||||
export class FarcasterProvider extends AuthProviderAbstract {
|
||||
generateLink() {
|
||||
return '';
|
||||
}
|
||||
|
||||
async getToken(code: string) {
|
||||
async getToken(code: string, _redirectUri?: string) {
|
||||
const data = JSON.parse(Buffer.from(code, 'base64').toString());
|
||||
const status = await client.lookupSigner({ signerUuid: data.signer_uuid });
|
||||
if (status.status === 'approved') {
|
||||
|
|
@ -29,11 +33,6 @@ export class FarcasterProvider implements ProvidersInterface {
|
|||
};
|
||||
}
|
||||
|
||||
// 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,6 +1,10 @@
|
|||
import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.interface';
|
||||
import {
|
||||
AuthProvider,
|
||||
AuthProviderAbstract,
|
||||
} from '@gitroom/backend/services/auth/providers.interface';
|
||||
|
||||
export class GithubProvider implements ProvidersInterface {
|
||||
@AuthProvider({ provider: 'GITHUB' })
|
||||
export class GithubProvider extends AuthProviderAbstract {
|
||||
generateLink(): string {
|
||||
return `https://github.com/login/oauth/authorize?client_id=${
|
||||
process.env.GITHUB_CLIENT_ID
|
||||
|
|
@ -9,7 +13,7 @@ export class GithubProvider implements ProvidersInterface {
|
|||
)}`;
|
||||
}
|
||||
|
||||
async getToken(code: string): Promise<string> {
|
||||
async getToken(code: string, _redirectUri?: string): Promise<string> {
|
||||
const { access_token } = await (
|
||||
await fetch('https://github.com/login/oauth/access_token', {
|
||||
method: 'POST',
|
||||
|
|
|
|||
|
|
@ -1,45 +1,28 @@
|
|||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { google } from 'googleapis';
|
||||
import { OAuth2Client } from 'google-auth-library/build/src/auth/oauth2client';
|
||||
import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.interface';
|
||||
import {
|
||||
AuthProvider,
|
||||
AuthProviderAbstract,
|
||||
} from '@gitroom/backend/services/auth/providers.interface';
|
||||
|
||||
const clientAndYoutube = () => {
|
||||
const client = new google.auth.OAuth2({
|
||||
const defaultRedirect = () =>
|
||||
`${process.env.FRONTEND_URL}/integrations/social/youtube`;
|
||||
|
||||
const makeClient = (redirectUri: string) =>
|
||||
new google.auth.OAuth2({
|
||||
clientId: process.env.YOUTUBE_CLIENT_ID,
|
||||
clientSecret: process.env.YOUTUBE_CLIENT_SECRET,
|
||||
redirectUri: `${process.env.FRONTEND_URL}/integrations/social/youtube`,
|
||||
redirectUri,
|
||||
});
|
||||
|
||||
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({
|
||||
@AuthProvider({ provider: 'GOOGLE' })
|
||||
export class GoogleProvider extends AuthProviderAbstract {
|
||||
generateLink(query?: { redirect_uri?: string }) {
|
||||
const redirectUri = query?.redirect_uri || defaultRedirect();
|
||||
return makeClient(redirectUri).generateAuthUrl({
|
||||
access_type: 'online',
|
||||
prompt: 'consent',
|
||||
state,
|
||||
redirect_uri: `${process.env.FRONTEND_URL}/integrations/social/youtube`,
|
||||
state: 'login',
|
||||
redirect_uri: redirectUri,
|
||||
scope: [
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
|
|
@ -47,21 +30,22 @@ export class GoogleProvider implements ProvidersInterface {
|
|||
});
|
||||
}
|
||||
|
||||
async getToken(code: string) {
|
||||
const { client, oauth2 } = clientAndYoutube();
|
||||
async getToken(code: string, redirectUri?: string) {
|
||||
const client = makeClient(redirectUri || defaultRedirect());
|
||||
const { tokens } = await client.getToken(code);
|
||||
return tokens.access_token;
|
||||
return tokens.access_token!;
|
||||
}
|
||||
|
||||
async getUser(providerToken: string) {
|
||||
const { client, oauth2 } = clientAndYoutube();
|
||||
const client = makeClient(defaultRedirect());
|
||||
client.setCredentials({ access_token: providerToken });
|
||||
const user = oauth2(client);
|
||||
const { data } = await user.userinfo.get();
|
||||
const { data } = await google
|
||||
.oauth2({ version: 'v2', auth: client })
|
||||
.userinfo.get();
|
||||
|
||||
return {
|
||||
id: data.id!,
|
||||
email: data.email,
|
||||
email: data.email!,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,66 +1,56 @@
|
|||
import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.interface';
|
||||
import {
|
||||
AuthProvider,
|
||||
AuthProviderAbstract,
|
||||
} from '@gitroom/backend/services/auth/providers.interface';
|
||||
|
||||
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() {
|
||||
@AuthProvider({ provider: 'GENERIC' })
|
||||
export class OauthProvider extends AuthProviderAbstract {
|
||||
private getConfig() {
|
||||
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)
|
||||
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');
|
||||
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');
|
||||
}
|
||||
|
||||
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;
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
generateLink(): string {
|
||||
const { authUrl, clientId, frontendUrl } = this.getConfig();
|
||||
const params = new URLSearchParams({
|
||||
client_id: this.clientId,
|
||||
client_id: clientId,
|
||||
scope: 'openid profile email',
|
||||
response_type: 'code',
|
||||
redirect_uri: `${this.frontendUrl}/settings`,
|
||||
redirect_uri: `${frontendUrl}/settings`,
|
||||
});
|
||||
|
||||
return `${this.authUrl}?${params.toString()}`;
|
||||
return `${authUrl}?${params.toString()}`;
|
||||
}
|
||||
|
||||
async getToken(code: string): Promise<string> {
|
||||
const response = await fetch(`${this.tokenUrl}`, {
|
||||
async getToken(code: string, _redirectUri?: string): Promise<string> {
|
||||
const { tokenUrl, clientId, clientSecret, frontendUrl } = this.getConfig();
|
||||
const response = await fetch(`${tokenUrl}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
|
|
@ -68,10 +58,10 @@ export class OauthProvider implements ProvidersInterface {
|
|||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
client_id: this.clientId,
|
||||
client_secret: this.clientSecret,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
code,
|
||||
redirect_uri: `${this.frontendUrl}/settings`,
|
||||
redirect_uri: `${frontendUrl}/settings`,
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -85,7 +75,8 @@ export class OauthProvider implements ProvidersInterface {
|
|||
}
|
||||
|
||||
async getUser(access_token: string): Promise<{ email: string; id: string }> {
|
||||
const response = await fetch(`${this.userInfoUrl}`, {
|
||||
const { userInfoUrl } = this.getConfig();
|
||||
const response = await fetch(`${userInfoUrl}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
Accept: 'application/json',
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
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,16 +1,17 @@
|
|||
import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.interface';
|
||||
import {
|
||||
AuthProvider,
|
||||
AuthProviderAbstract,
|
||||
} 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.');
|
||||
}
|
||||
|
|
@ -19,16 +20,15 @@ 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;
|
||||
}
|
||||
|
||||
export class WalletProvider implements ProvidersInterface {
|
||||
@AuthProvider({ provider: 'WALLET' })
|
||||
export class WalletProvider extends AuthProviderAbstract {
|
||||
async generateLink(params: { publicKey: string }) {
|
||||
if (!params.publicKey) {
|
||||
return;
|
||||
|
|
@ -40,7 +40,7 @@ export class WalletProvider implements ProvidersInterface {
|
|||
return challenge;
|
||||
}
|
||||
|
||||
async getToken(code: string) {
|
||||
async getToken(code: string, _redirectUri?: string) {
|
||||
const { publicKey, challenge, signature } = JSON.parse(
|
||||
Buffer.from(code, 'base64').toString()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
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) {}
|
||||
constructor(
|
||||
private _organizationService: OrganizationService,
|
||||
private _oauthService: OAuthService
|
||||
) {}
|
||||
async use(req: Request, res: Response, next: NextFunction) {
|
||||
const auth = (req.headers.authorization ||
|
||||
req.headers.Authorization) as string;
|
||||
|
|
@ -14,21 +18,44 @@ export class PublicAuthMiddleware implements NestMiddleware {
|
|||
return;
|
||||
}
|
||||
try {
|
||||
const org = await this._organizationService.getOrgByApiKey(auth);
|
||||
if (!org) {
|
||||
res.status(HttpStatus.UNAUTHORIZED).json({ msg: 'Invalid API key' });
|
||||
return;
|
||||
}
|
||||
if (auth.startsWith('pos_')) {
|
||||
const authorization = await this._oauthService.getOrgByOAuthToken(auth);
|
||||
if (!authorization) {
|
||||
res
|
||||
.status(HttpStatus.UNAUTHORIZED)
|
||||
.json({ msg: 'Invalid OAuth token' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!!process.env.STRIPE_SECRET_KEY && !org.subscription) {
|
||||
res
|
||||
.status(HttpStatus.UNAUTHORIZED)
|
||||
.json({ msg: 'No subscription found' });
|
||||
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' } }] };
|
||||
// @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' } }] };
|
||||
}
|
||||
} catch (err) {
|
||||
throw new HttpForbiddenException();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
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, BullMqModule, AgentModule],
|
||||
imports: [ExternalCommandModule, DatabaseModule, AgentModule],
|
||||
controllers: [],
|
||||
providers: [CheckStars, RefreshTokens, ConfigurationTask, AgentRun],
|
||||
providers: [RefreshTokens, ConfigurationTask, AgentRun],
|
||||
get exports() {
|
||||
return [...this.imports, ...this.providers];
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"name": "postiz-cron",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"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/cron/src/main.js",
|
||||
"pm2": "pm2 start pnpm --name cron -- start"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
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 {}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { initializeSentry } from '@gitroom/nestjs-libraries/sentry/initialize.sentry';
|
||||
initializeSentry('cron');
|
||||
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { CronModule } from './cron.module';
|
||||
|
||||
async function bootstrap() {
|
||||
// some comment again
|
||||
await NestFactory.createApplicationContext(CronModule);
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
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'),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
Executable file → Normal file
37
apps/extension/manifest.dev.json
Executable file → Normal file
|
|
@ -1,15 +1,30 @@
|
|||
{
|
||||
"action": {
|
||||
"default_icon": "public/dev-icon-32.png",
|
||||
"default_popup": "src/pages/popup/index.html"
|
||||
},
|
||||
"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",
|
||||
"icons": {
|
||||
"128": "public/dev-icon-128.png"
|
||||
"32": "icon-32.png",
|
||||
"128": "icon-128.png"
|
||||
},
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["contentStyle.css", "dev-icon-128.png", "dev-icon-32.png"],
|
||||
"matches": []
|
||||
}
|
||||
]
|
||||
"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/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,29 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Postiz",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"version": "2.0.0",
|
||||
"description": "Postiz browser extension for social media scheduling",
|
||||
"icons": {
|
||||
"32": "icon-32.png",
|
||||
"128": "icon-128.png"
|
||||
},
|
||||
"permissions": ["activeTab", "cookies", "tabs"],
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["http://*/*", "https://*/*", "<all_urls>"],
|
||||
"js": ["src/pages/content/index.tsx"],
|
||||
"css": ["contentStyle.css"]
|
||||
}
|
||||
"permissions": [
|
||||
"cookies",
|
||||
"alarms",
|
||||
"storage"
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["contentStyle.css", "icon-128.png", "icon-32.png"],
|
||||
"matches": []
|
||||
}
|
||||
]
|
||||
"host_permissions": [
|
||||
"*://*.skool.com/*"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js",
|
||||
"type": "module"
|
||||
},
|
||||
"externally_connectable": {
|
||||
"matches": [
|
||||
"http://localhost/*",
|
||||
"https://localhost/*",
|
||||
"https://*.postiz.com/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"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,14 +1,10 @@
|
|||
{
|
||||
"name": "postiz-extension",
|
||||
"version": "1.0.3",
|
||||
"description": "A simple chrome & firefox extension template with Vite, React, TypeScript and Tailwind CSS.",
|
||||
"version": "2.0.0",
|
||||
"description": "Postiz browser extension for cookie-based platform authentication",
|
||||
"scripts": {
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 5.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1 KiB |
|
|
@ -1,7 +0,0 @@
|
|||
<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>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
|
|
@ -1,13 +0,0 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@theme {
|
||||
--animate-spin-slow: spin 20s linear infinite;
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
209
apps/extension/src/background.ts
Normal file
209
apps/extension/src/background.ts
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
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
11
apps/extension/src/global.d.ts
vendored
|
|
@ -1,11 +0,0 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"extName": {
|
||||
"message": "name in src/locales/en/messages.json",
|
||||
"description": "Extension name"
|
||||
},
|
||||
"extDescription": {
|
||||
"message": "description in src/locales/en/messages.json",
|
||||
"description": "Extension description"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
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;
|
||||
});
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
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>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
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 />);
|
||||
|
|
@ -1,191 +0,0 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
@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;
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
.container {
|
||||
width: 100%;
|
||||
height: 50vh;
|
||||
font-size: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import React from 'react';
|
||||
import '@gitroom/extension/pages/options/Options.css';
|
||||
|
||||
export default function Options() {
|
||||
return <div className="container">Options</div>;
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
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();
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
body {
|
||||
background-color: #242424;
|
||||
}
|
||||
|
||||
.container {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import React from 'react';
|
||||
import '@pages/panel/Panel.css';
|
||||
|
||||
export default function Panel() {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>Side Panel</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import Panel from '@pages/panel/Panel';
|
||||
import '@pages/panel/index.css';
|
||||
import '@assets/styles/tailwind.css';
|
||||
|
||||
function init() {
|
||||
const rootContainer = document.querySelector('#__root');
|
||||
if (!rootContainer) throw new Error("Can't find Panel root element");
|
||||
const root = createRoot(rootContainer);
|
||||
root.render(<Panel />);
|
||||
}
|
||||
|
||||
init();
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ProviderList } from '@gitroom/extension/providers/provider.list';
|
||||
import { fetchCookie } from '@gitroom/extension/utils/load.cookie';
|
||||
|
||||
export const PopupContainerContainer: FC = () => {
|
||||
const [url, setUrl] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
|
||||
setUrl(tabs[0]?.url);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!url) {
|
||||
return (
|
||||
<div className="text-4xl">This website is not supported by Postiz</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <PopupContainer url={url} />;
|
||||
};
|
||||
|
||||
export const PopupContainer: FC<{ url: string }> = (props) => {
|
||||
const { url } = props;
|
||||
const [isLoggedIn, setIsLoggedIn] = useState<false | string>(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const provider = useMemo(() => {
|
||||
return ProviderList.find((p) => {
|
||||
return p.baseUrl.indexOf(new URL(url).hostname) > -1;
|
||||
});
|
||||
}, [url]);
|
||||
|
||||
const loadCookie = useCallback(async () => {
|
||||
try {
|
||||
if (!provider) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
const auth = await fetchCookie(`auth`);
|
||||
|
||||
if (auth) {
|
||||
setIsLoggedIn(auth);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadCookie();
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!provider) {
|
||||
return (
|
||||
<div className="text-4xl">This website is not supported by Postiz</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return <div className="text-4xl">You are not logged in to Postiz</div>;
|
||||
}
|
||||
|
||||
return <div />;
|
||||
};
|
||||
|
||||
export default function Popup() {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-screen">
|
||||
<PopupContainerContainer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
width: 300px;
|
||||
height: 260px;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Popup</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="__root"></div>
|
||||
<script type="module" src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import './index.css';
|
||||
import '@gitroom/extension/assets/styles/tailwind.css';
|
||||
import Popup from '@gitroom/extension/pages/popup/Popup';
|
||||
|
||||
function init() {
|
||||
const rootContainer = document.querySelector('#__root');
|
||||
if (!rootContainer) throw new Error("Can't find Popup root element");
|
||||
const root = createRoot(rootContainer);
|
||||
root.render(<Popup />);
|
||||
}
|
||||
|
||||
init();
|
||||
19
apps/extension/src/providers/cookie-provider.interface.ts
Normal file
19
apps/extension/src/providers/cookie-provider.interface.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
export interface CookieDefinition {
|
||||
/** The cookie name to extract, e.g., 'client_id' */
|
||||
name: string;
|
||||
/** Whether this cookie must exist for the extraction to be considered successful */
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
export interface CookieProvider {
|
||||
/** Unique identifier used in messages, e.g., 'skool' */
|
||||
identifier: string;
|
||||
/** Human-readable name, e.g., 'Skool' */
|
||||
name: string;
|
||||
/** URL to query cookies for, e.g., 'https://www.skool.com' — passed to chrome.cookies.getAll({ url }) */
|
||||
url: string;
|
||||
/** URL pattern for host_permissions in manifest, e.g., '*://*.skool.com/*' */
|
||||
hostPermission: string;
|
||||
/** List of cookies to extract from this site */
|
||||
cookies: CookieDefinition[];
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { ProviderInterface } from '@gitroom/extension/providers/provider.interface';
|
||||
|
||||
export class LinkedinProvider implements ProviderInterface {
|
||||
identifier = 'linkedin';
|
||||
baseUrl = 'https://www.linkedin.com';
|
||||
element = `.share-box-feed-entry__closed-share-box`;
|
||||
attachTo = `[role="main"]`;
|
||||
style = 'light' as 'light';
|
||||
findIdentifier = (element: HTMLElement) => {
|
||||
return element.closest('[data-urn]').getAttribute('data-urn');
|
||||
};
|
||||
}
|
||||
12
apps/extension/src/providers/list/skool.provider.ts
Normal file
12
apps/extension/src/providers/list/skool.provider.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { CookieProvider } from '../cookie-provider.interface';
|
||||
|
||||
export const skoolProvider: CookieProvider = {
|
||||
identifier: 'skool',
|
||||
name: 'Skool',
|
||||
url: 'https://www.skool.com',
|
||||
hostPermission: '*://*.skool.com/*',
|
||||
cookies: [
|
||||
{ name: 'client_id', required: true },
|
||||
{ name: 'auth_token', required: true },
|
||||
],
|
||||
};
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import { ProviderInterface } from '@gitroom/extension/providers/provider.interface';
|
||||
|
||||
export class XProvider implements ProviderInterface {
|
||||
identifier = 'x';
|
||||
baseUrl = 'https://x.com';
|
||||
element = `[data-testid="primaryColumn"]:has([data-testid="toolBar"]) [data-testid="tweetTextarea_0_label"], [data-testid="SideNav_NewTweet_Button"]`;
|
||||
attachTo = `#react-root`;
|
||||
style = 'dark' as 'dark';
|
||||
findIdentifier = (element: HTMLElement) => {
|
||||
return (
|
||||
Array.from(
|
||||
(
|
||||
element?.closest('article') ||
|
||||
element?.closest(`[aria-labelledby="modal-header"]`)
|
||||
)?.querySelectorAll('a') || []
|
||||
)
|
||||
?.find((p) => {
|
||||
return p?.getAttribute('href')?.includes('/status/');
|
||||
})
|
||||
?.getAttribute('href')
|
||||
?.split('/status/')?.[1] || window.location.href.split('/status/')?.[1]
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
export interface ProviderInterface {
|
||||
identifier: string;
|
||||
baseUrl: string;
|
||||
element: string;
|
||||
findIdentifier: (element: HTMLElement) => string;
|
||||
attachTo: string;
|
||||
style: 'dark' | 'light';
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue