Compare commits

...
Sign in to create a new pull request.

531 commits

Author SHA1 Message Date
Nevo David
09088a5391 Merge remote-tracking branch 'origin/main'
Some checks failed
Build / build (22.12.0) (push) Has been cancelled
Code Quality Analysis / Analyze (javascript-typescript) (push) Has been cancelled
2026-05-18 20:49:30 +07:00
Nevo David
faeb89853b feat: threads error 2026-05-18 20:32:45 +07:00
Nevo David
6fc51da7e3
Merge pull request #1538 from gitroomhq/feat/list-view-post-filters
Add state filter (all/scheduled/draft/published) to list view
2026-05-18 20:24:39 +07:00
Santosh Bhandari
415c9c4ba8
Merge pull request #1537 from gitroomhq/tiktok-info
Show TikTok title/content restriction notice for video posts
2026-05-18 10:29:12 +00:00
Santosh Bhandari
e19c855da6
Merge pull request #1539 from gitroomhq/fix/tiktok-pending-share-error-message
Clarify TikTok pending-share error mentions the 24-hour window
2026-05-18 10:27:59 +00:00
Santosh Bhandari
7e0bb7075e fix: clarify TikTok pending-share error mentions 24-hour window
Some checks failed
Build / build (22.12.0) (push) Has been cancelled
TikTok's spam_risk_too_many_pending_share limit applies per 24-hour
period; the previous message did not state the window.
2026-05-18 15:28:03 +05:45
Nevo David
0b3328daeb feat: pinterest fixes 2026-05-18 16:39:38 +07:00
Santosh Bhandari
4811741e63 feat: filter list view by post state (all/scheduled/draft/published)
Adds a state filter to the calendar list view so users can see all
posts (default) or narrow to scheduled, draft, or published. The
backend repository now switches its WHERE/orderBy off the new query
param; 'all' includes ERROR posts so failed publishes remain visible.
2026-05-18 15:21:28 +05:45
Santosh Bhandari
7dda2812d7 feat: add TikTok restriction notice for video posts
Show an inline warning explaining title/content limitations for
direct-post vs upload-only video modes on TikTok.
2026-05-18 14:44:25 +05:45
Nevo David
2316a45388 feat: upgrade nextjs due to security risks 2026-05-18 13:55:12 +07:00
Nevo David
38b0ac8c70 feat: update nestjs 2026-05-15 16:15:50 +07:00
Nevo David
17fa64726c feat: tracking 2026-05-14 18:10:01 +07:00
Nevo David
03ddef66e2 feat: trial tracker 2026-05-14 17:43:42 +07:00
Nevo David
0dce16029e feat: google tag 2026-05-14 17:32:54 +07:00
Nevo David
03aa6b13dd Merge remote-tracking branch 'origin/main' 2026-05-14 16:51:27 +07:00
Nevo David
630602858e feat: gtm 2026-05-14 16:50:01 +07:00
Nevo David
715d3e40fd
Merge pull request #1514 from gitroomhq/feat/creation-method-tracking
feat: track post creation method (WEB/API/MCP/AUTOPOST)
2026-05-14 12:40:16 +07:00
Santosh Bhandari
e63d6d2cf2 feat: restrict public API creation methods to CLI and API 2026-05-14 10:06:58 +05:45
Nevo David
f2ebadab9e
Merge pull request #1515 from gitroomhq/feat/has-extension-helper
feat: hasExtension helper for media type detection
2026-05-14 11:13:36 +07:00
Nevo David
1677714670
Merge branch 'main' into feat/has-extension-helper 2026-05-14 11:13:12 +07:00
Enno Gelhaus
b4635f026b
feat: increase default api rate limit to 90 2026-05-13 17:43:22 +02:00
Nevo David
5f2f5581b2 feat: 3d secure fix 2026-05-13 19:53:53 +07:00
Nevo David
7cc3d9bd78 feat: stripe fix 2026-05-13 09:01:22 +07:00
Nevo David
d2c1eabc8b feat: fix workflow after sleep 2026-05-12 23:57:13 +07:00
Nevo David
4ee5231cb2 Merge remote-tracking branch 'origin/main' 2026-05-12 23:34:02 +07:00
Nevo David
16abf0dc9a feat: posting if no subscription 2026-05-12 23:33:49 +07:00
Enno Gelhaus
86368d7b7b
feat: upgrade ci pnpm 2026-05-12 15:13:56 +02:00
Nevo David
e986d9e493 feat: strip links 2026-05-12 19:51:23 +07:00
Santosh Bhandari
aa0c16b648 feat: accept CLI creation method via public API 2026-05-12 17:25:10 +05:45
Santosh Bhandari
510f396389 feat: show creation method badge only when impersonating 2026-05-12 17:06:29 +05:45
Santosh Bhandari
80b6bdcabe feat: hasExtension helper for media type detection 2026-05-12 16:23:11 +05:45
Santosh Bhandari
e153ab0a9b feat: track post creation method (WEB/API/MCP/AUTOPOST) 2026-05-12 14:41:59 +05:45
Nevo David
cf0ab36a23 feat: deletedAt integrations ignore 2026-05-12 13:27:58 +07:00
Nevo David
009bd36528 feat: fix tiktok url ownership error 2026-05-11 17:50:14 +07:00
Nevo David
905392513f feat: no post 2026-05-11 14:16:08 +07:00
Nevo David
7be292094a feat: no post 2026-05-11 14:15:33 +07:00
Nevo David
6e55eb3b92 feat: more cover for pending queues 2026-05-11 13:57:46 +07:00
Nevo David
39f2a176e1 Merge remote-tracking branch 'origin/main' 2026-05-11 11:19:41 +07:00
Nevo David
638b071283 feat: corrupted file 2026-05-11 11:19:02 +07:00
Nevo David
060c77a68c
Merge pull request #1488 from gitroomhq/fix/ui-modal-max-width
Some checks failed
Build / build (22.12.0) (push) Has been cancelled
Code Quality Analysis / Analyze (javascript-typescript) (push) Has been cancelled
ui: update the modal such that for long text won't cause overflow
2026-05-07 10:40:39 +07:00
Nevo David
d4405906bd
Merge pull request #1489 from gitroomhq/feat/ui-notification-time
feat: update notification list to be scrollable and added time
2026-05-07 10:40:21 +07:00
Nevo David
c8f1074f48
Merge pull request #1494 from gitroomhq/fix/registration-email-lowercase
fix: lowercase email on local registration
2026-05-06 12:10:52 +07:00
Santosh Bhandari
dcb1b0188a fix: lowercase email on local registration 2026-05-06 10:46:25 +05:45
Enno Gelhaus
22f436e72e
feat: simplify pr template 2026-05-04 21:00:24 +02:00
Enno Gelhaus
53f0967e67
feat: merge queue 2026-05-04 15:50:13 +02:00
Santosh Bhandari
18a1a80871 feat: update notification list to be scrollable and added time 2026-05-04 19:05:21 +05:45
Santosh Bhandari
a6967c8519 ui: update the modal such that for long text won't cause overflow 2026-05-04 18:41:34 +05:45
Enno Gelhaus
7e92764ad2
Merge pull request #1482 from gitroomhq/feat/contribute-checker
feat: contributor form
2026-05-04 10:29:58 +02:00
Enno Gelhaus
1bf32426c7
Update contribution form link in CONTRIBUTING.md 2026-05-04 10:29:09 +02:00
Enno Gelhaus
779764aa5d
Fix contribution form link in PR template
Updated contribution form link in PR template.
2026-05-04 10:28:48 +02:00
Nevo David
232ebb2528
Merge pull request #1483 from gitroomhq/fix/linkedin-gif-sharp-compress-removal
fix: remove processing GIF via sharp in linkedin
2026-05-04 15:03:25 +07:00
Santosh Bhandari
d056225053 fix: remove processing GIF via sharp in linkedin 2026-05-04 08:09:25 +05:45
Enno Gelhaus
e419e05f09
Apply suggestions from code review
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-03 16:27:56 +02:00
Enno Gelhaus
47ce014204
feat: contributor form 2026-05-03 14:17:54 +02:00
Nevo David
9d14b0262d Merge remote-tracking branch 'origin/main' 2026-05-02 14:00:32 +07:00
Nevo David
971042a074 feat: shrink workflow payload 2026-05-02 14:00:09 +07:00
Nevo David
c3976e554f
Merge pull request #1479 from gitroomhq/fix/discord-provider-error-handler
fix: properly handle error in discord provider
2026-05-01 09:42:49 +07:00
Santosh Bhandari
ef111eb1c4 fix: update error message 2026-04-30 21:45:25 +05:45
Enno Gelhaus
3ee35a7348
feat: temporarily disable cache to fix build 2026-04-30 17:52:36 +02:00
Santosh Bhandari
d6bc6eb0ff fix: properly handle error in discord provider 2026-04-30 21:28:11 +05:45
Nevo David
0d98fc02fb feat: X errors and force upload
Some checks failed
Build / build (22.12.0) (push) Has been cancelled
Code Quality Analysis / Analyze (javascript-typescript) (push) Has been cancelled
2026-04-30 18:34:18 +07:00
Nevo David
7264c00298 feat: err 2026-04-30 17:44:06 +07:00
Nevo David
bb7cd46a4f feat: errors 2026-04-30 17:24:01 +07:00
Nevo David
7236213ea4 feat: fix xss
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-04-27 14:50:55 +07:00
Nevo David
4bdcfec3d7 Merge remote-tracking branch 'origin/main' 2026-04-27 13:36:56 +07:00
Nevo David
cdcf63bf6b feat: when creating a postiz app, allow localhost 2026-04-27 13:17:12 +07:00
Enno Gelhaus
fd6553196b
Merge pull request #1465 from mousaa/pr/x-url-env
feat: add X_URL env var to docker-compose and .env.example
2026-04-26 14:49:40 +02:00
Nevo David
90b2581048 feat: better information on use Postiz for developers 2026-04-26 15:04:01 +07:00
Nevo David
4e7864c929 feat: better MCP options 2026-04-26 14:58:19 +07:00
Ahmed Mousa
b91ffdc9c3 fix: remove duplicate X_URL entry in docker-compose
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 16:17:55 -04:00
Ahmed Mousa
d75662b56a feat: add X_URL to docker-compose and .env.example
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 16:13:58 -04:00
Nevo David
876be6f8c6 Merge remote-tracking branch 'origin/main' 2026-04-22 22:26:50 +07:00
Nevo David
071143dcb0 feat: security fix 2026-04-22 22:25:48 +07:00
Enno Gelhaus
da448012dd
feat: remove insecure & unnecessary workflow. 2026-04-22 16:20:52 +02:00
Nevo David
e51cae1614 feat: fix analytics 2026-04-22 13:09:39 +07:00
Nevo David
fa5d7f4c40 feat: fix pop 2026-04-22 11:09:42 +07:00
Nevo David
0b554e6844 Merge remote-tracking branch 'origin/main' 2026-04-20 12:27:13 +07:00
Nevo David
8951289426 feat: fix previous agent messages 2026-04-20 12:26:56 +07:00
Enno Gelhaus
ec4759e934
feat: include cloud in security scope 2026-04-20 03:11:24 +02:00
Enno Gelhaus
c61e061145
feat: security refinement 2026-04-19 23:09:01 +02:00
Enno Gelhaus
8cfb634b66
feat: security rewording 2026-04-19 23:00:16 +02:00
Enno Gelhaus
55a542485a
feat: security additions 2026-04-19 22:58:05 +02:00
Enno Gelhaus
0eddfb3304
feat: security policy changes
- Add Response Timelines
- Add Security Scope
- Modify maintainer list for security maintainers
2026-04-19 21:13:12 +02:00
Nevo David
88006a7614 feat: mobile app iframe 2026-04-19 13:48:15 +07:00
Nevo David
45e55c545d feat: add auth header 2026-04-18 19:53:19 +07:00
Nevo David
0a8fa5bff6 feat: fix bridge 2026-04-18 18:36:57 +07:00
Nevo David
65d23707ab feat: fix bridge 2026-04-18 18:20:18 +07:00
Nevo David
846954f059 feat: bridge fix 2026-04-18 18:00:22 +07:00
Nevo David
c79965718f feat: auth changes 2026-04-18 17:14:10 +07:00
Nevo David
027c9caa96 feat: fix check validity 2026-04-18 02:18:28 +07:00
Nevo David
8a7e8eb8f3 feat: empty object 2026-04-18 01:16:32 +07:00
Nevo David
71b2e2e793 feat: fix validity 2026-04-18 00:17:05 +07:00
Nevo David
2ae293916b feat: fix validity 2026-04-18 00:12:41 +07:00
Nevo David
e5947034ab feat: check validity 2026-04-17 23:53:29 +07:00
Nevo David
0ecca5298d feat: remove gap 2026-04-17 22:41:30 +07:00
Nevo David
7d9b99abf3 feat: provider edit preview 2026-04-17 21:55:47 +07:00
Nevo David
45bdf128e9 feat: oauth mobile callback 2026-04-17 18:02:54 +07:00
Nevo David
4e277ed32d feat: load first 2026-04-17 17:52:15 +07:00
Nevo David
5257f2fabe feat: redirect url 2026-04-17 17:34:34 +07:00
Nevo David
386fc7b049 feat: fix generation 2026-04-15 17:53:37 +07:00
Nevo David
ec8c0f6fb9 feat: search in media 2026-04-14 20:35:17 +07:00
Nevo David
eb0334d96d Merge remote-tracking branch 'origin/main' 2026-04-13 23:31:47 +07:00
Nevo David
288a4d428b feat: change post status 2026-04-13 23:31:08 +07:00
Enno Gelhaus
1145e51ea6
feat: sentry replay 2026-04-13 07:10:56 +02:00
Nevo David
3ea302202d feat: fix advisory
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-04-12 10:27:01 +07:00
Nevo David
e3b3b82fae feat: instagram better error 2026-04-10 20:17:38 +07:00
Nevo David
318e9da8d1 feat: refresh token, show error 2026-04-10 19:08:09 +07:00
Nevo David
186d3370e9 feat: instagram refresh fix 2026-04-10 18:33:19 +07:00
Nevo David
98c32c3da5 feat: subreddit posting fix with api 2026-04-10 16:27:34 +07:00
Nevo David
3e8a0ab817 feat: langchain upgrade
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-04-09 18:28:44 +07:00
Nevo David
6a06b210c2 Merge remote-tracking branch 'origin/main' 2026-04-09 18:11:21 +07:00
Nevo David
30e8b77709 feat: security fixes 2026-04-09 18:11:07 +07:00
Enno Gelhaus
59e535e258
fix: pr quality 2026-04-08 12:25:49 +02:00
Enno Gelhaus
a8918a6284
feat: update pr quality 2026-04-08 12:24:30 +02:00
Nevo David
26d7ffa0ed Merge remote-tracking branch 'origin/main' 2026-04-07 18:05:44 +07:00
Nevo David
c7867ab05e feat: download invoices 2026-04-07 18:05:30 +07:00
Nevo David
9b50a18f2d
Merge pull request #1383 from swaraj017/debug/youtube-refresh-token
fix(youtube): preserve existing refresh token to avoid daily re-auth
2026-04-07 10:57:28 +07:00
swaraj017
943293dc7d fix(youtube): preserve existing refresh token to avoid daily re-auth 2026-04-06 23:33:26 +05:30
Enno Gelhaus
61a563a06c
Merge pull request #1369 from fizikiukas/patch-1
Update Hostinger link in README.md
2026-04-04 13:14:39 +02:00
Enno Gelhaus
76676510f2
Merge branch 'main' of https://github.com/gitroomhq/postiz-app 2026-04-04 12:46:54 +02:00
Enno Gelhaus
af8d04002c
feat: gitignore 2026-04-04 12:46:50 +02:00
Nevo David
d16945a9e2 feat: fix cli + server openai token 2026-04-04 15:00:27 +07:00
Nevo David
fb43544bb2 feat: fix parseR 2026-04-04 12:56:05 +07:00
Nevo David
008deb0daf feat: update axios version 2026-04-04 12:05:11 +07:00
Nevo David
c5882d3eae feat: auto accept and json response 2026-04-03 22:52:02 +07:00
Nevo David
1bbfa8d603 feat: fix header for all agents 2026-04-03 22:25:24 +07:00
Nevo David
6bf80ff846 feat: upgrade mcp 2026-04-03 21:48:41 +07:00
Nevo David
f55cca519c feat: override 2026-04-03 19:39:29 +07:00
Nevo David
ac109bf564 feat: mcp with oauth 2026-04-03 19:17:19 +07:00
Nevo David
5d46a6cd34 feat: new mastra 2026-04-03 18:39:33 +07:00
Nevo David
507a006b9f Merge remote-tracking branch 'origin/main' 2026-04-03 13:34:36 +07:00
Nevo David
073479bc4a feat: pinterest, show more boards 2026-04-03 13:34:24 +07:00
Valentinas Čirba
687925bcbf
Update Hostinger link in README.md 2026-04-03 08:35:00 +03:00
Nevo David
2d434fe198
Merge pull request #1359 from gitroomhq/feat/orchestrator-sentry
Feat/orchestrator sentry
2026-04-02 14:33:13 +07:00
Enno Gelhaus
e97be93dc4
revert: add sentry to orchestrator appmodule 2026-04-01 20:40:53 +02:00
Enno Gelhaus
bb63a7c8a9
feat: add sentry to orchestrator appmodule 2026-04-01 20:34:57 +02:00
Enno Gelhaus
0f4db4a375
feat: add sentry in orchestrator 2026-04-01 20:32:13 +02:00
Nevo David
3a376f4a9c feat: health check for temporal 2026-03-31 23:12:12 +07:00
Nevo David
afda06bb6a feat: show port 2026-03-31 21:36:37 +07:00
Nevo David
1840a6db6d feat: remove whitespace 2026-03-31 21:13:36 +07:00
Nevo David
b0832740d0 feat: developers UI, description 2026-03-31 13:42:44 +07:00
Nevo David
40d9d11f72 feat: add developers sections for non paying customers 2026-03-31 12:00:11 +07:00
Nevo David
7b67b4a6b5 Merge remote-tracking branch 'origin/main' 2026-03-31 11:38:09 +07:00
Nevo David
8c03078086 feat: fix integrations + import option + reelfarm 2026-03-31 11:37:51 +07:00
Enno Gelhaus
5ae4c950db
feat: secure /webhooks with IsSafeWebhookUrl
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-03-29 17:56:45 +02:00
Nevo David
6f7a80f689 feat: steam url fix
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-03-29 13:41:13 +07:00
Nevo David
52f59bcfde feat: steam url fix 2026-03-29 13:40:05 +07:00
Nevo David
13fedeca8b feat: upload from url, prevent internal access 2026-03-29 12:32:59 +07:00
Nevo David
f55253d1ab feat: refresh channel fix 2026-03-29 10:14:24 +07:00
Nevo David
e6a019b778 feat: announcement 2026-03-27 09:47:43 +07:00
Nevo David
8bb1935834 feat: upgrade polotno 2026-03-26 20:51:06 +07:00
Nevo David
117dab594e feat: fix tiktok key 2026-03-26 11:35:04 +07:00
Nevo David
777ca3d329 feat: fix typing 2026-03-26 11:21:11 +07:00
Nevo David
666de6ac06 feat: import post 2026-03-26 11:11:55 +07:00
Nevo David
2786fbaeba feat(x): add with AI, paid partenership 2026-03-26 09:48:55 +07:00
Nevo David
6aa5bf3591 fix mention component 2026-03-26 00:50:55 +07:00
Nevo David
d8386efc52 fix: mark posts with errors 2026-03-25 23:00:12 +07:00
Nevo David
be5d871896 fix: autopost url
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-03-25 17:45:18 +07:00
Nevo David
0ad89ccd26 feat: protect webhooks 2026-03-25 17:39:24 +07:00
Nevo David
92b82837e9 feat: remove Image
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-03-25 16:51:13 +07:00
Nevo David
0c7000276d feat: change lang 2026-03-25 16:17:34 +07:00
Nevo David
a8c8428ae2 feat: fix direction 2026-03-25 16:08:30 +07:00
Nevo David
f675dd5179 feat: change dir 2026-03-25 15:57:46 +07:00
Nevo David
358bf62804 feat: layout 2026-03-25 15:17:35 +07:00
Nevo David
e621f965b5 feat: layout 2026-03-25 15:02:47 +07:00
Nevo David
072c6bac4a feat: fix language, dont cache cookies 2026-03-25 14:52:49 +07:00
Nevo David
c7a88b98bc feat: language switch 2026-03-25 14:42:00 +07:00
Nevo David
3bc3d004fa feat: language switch 2026-03-25 14:24:26 +07:00
Nevo David
afe06768fe feat: fix direction 2026-03-25 14:14:25 +07:00
Nevo David
41d48a81fd Merge remote-tracking branch 'origin/main' 2026-03-25 13:56:17 +07:00
Nevo David
b71297db57 feat: upgrade nextjs to the latest version 2026-03-25 13:55:45 +07:00
Enno Gelhaus
c280139917
feat: update security guidelines
- Add AI section
- Add PoC requirement
- Change reporting behavior
- Remove template
2026-03-23 18:31:23 +01:00
Nevo David
20053fc2f8
Remove Enterprise section from README
Removed the Postiz-as-a-service - Enterprise section from the README.
2026-03-22 13:32:31 +07:00
Nevo David
1aa47351a4 feat: revert all the mod changes 2026-03-22 10:23:52 +07:00
Nevo David
40c443ba96 feat: revert workflow| 2026-03-22 10:12:02 +07:00
Nevo David
cb8560e183 Merge remote-tracking branch 'origin/main' 2026-03-22 10:10:28 +07:00
Nevo David
9df6ab6b48 feat: tiktok 2026-03-22 10:10:14 +07:00
Enno Gelhaus
9a29793e2c
improve: sentry metrics 2026-03-21 23:30:20 +01:00
Enno Gelhaus
8d25914bd1
feat: sentry langchain 2026-03-21 23:07:42 +01:00
Enno Gelhaus
ac61be6454
fix: sentry metrics 2026-03-21 23:04:27 +01:00
Enno Gelhaus
8bbed0698d
Merge pull request #1331 from gitroomhq/feat/sentry-metrics
feat: a lot of sentry metrics
2026-03-21 22:34:22 +01:00
Enno Gelhaus
da995de88f
feat: security 2026-03-21 22:32:34 +01:00
Enno Gelhaus
1c04544c54
feat: a lot of sentry metrics 2026-03-21 22:18:28 +01:00
Enno Gelhaus
c539fa8cf4
feat: sentry v10.45 2026-03-21 14:46:29 +01:00
Nevo David
c164db76d9 Merge remote-tracking branch 'origin/main' 2026-03-21 16:32:48 +07:00
Nevo David
8dcfffff53 feat: added cus 2026-03-21 16:32:34 +07:00
Enno Gelhaus
b6d6b7283f
improve: PR Quality 2026-03-20 19:22:52 +01:00
Enno Gelhaus
33f3e552cb
fix: format in PR Quality 2026-03-20 19:16:55 +01:00
Enno Gelhaus
2f85198887
improve: PR Quality 2026-03-20 19:15:27 +01:00
Enno Gelhaus
2cd2911de2
Merge pull request #1326 from swaraj017/fix/redirect-register-to-login
fix: redirect /auth/register to /auth/login when registration is disabled
2026-03-20 16:27:10 +01:00
Enno Gelhaus
65939d3341
feat: improve PR Quality Workflow 2026-03-20 16:03:34 +01:00
Enno Gelhaus
eb14bd9f05
feat: Add AI section to PR Template 2026-03-20 15:58:25 +01:00
Enno Gelhaus
fa27e38ca0
feat: Add AI section to CONTRIBUTING.md 2026-03-20 15:57:17 +01:00
swaraj017
2f6896b332 fix: use correct DISABLE_REGISTRATION env variable 2026-03-18 14:59:17 +05:30
swaraj017
472ef851c2 fix: redirect /auth/register to /auth/login when registration is disabled 2026-03-18 12:45:43 +05:30
Nevo David
cf2a980dd8 feat: mewe
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-03-18 10:45:28 +07:00
Nevo David
2ae98a925a feat: fix adding integrations 2026-03-16 22:45:23 +07:00
Enno Gelhaus
e20565fb23
Add PR Quality workflow for pull request checks 2026-03-11 19:15:25 +01:00
Enno Gelhaus
3b79a74092
Merge pull request #1289 from Bramw2003/fix/skip-subscription-check-without-stripe
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
fix: skip subscription check when Stripe is not configured
2026-03-10 19:40:22 +01:00
Nevo David
08c3143647 feat: fix postiz oauth 2026-03-10 12:21:45 +07:00
Nevo David
41fcb08834 feat: postiz oauth fix 2026-03-10 12:05:21 +07:00
Nevo David
3d09e9f2c5 feat: fix oauth 2026-03-10 12:02:51 +07:00
Nevo David
f624321b43 feat: agent-media sso 2026-03-08 22:27:06 +07:00
Bram de Smidt
f1a32f243e
Merge branch 'main' into fix/skip-subscription-check-without-stripe 2026-03-06 19:46:52 +01:00
Nevo David
634bee898a feat: remove proxy 2026-03-06 21:44:35 +07:00
Nevo David
5e386155f9 feat: check proxy file 2026-03-06 21:26:55 +07:00
Nevo David
983286005a feat: change lock 2026-03-06 21:16:52 +07:00
Nevo David
3cf1cd05b3 feat: agent 2026-03-06 20:48:05 +07:00
Bram de Smidt
01fd2df995 fix: skip subscription check when Stripe is not configured
When STRIPE_SECRET_KEY is not set, posting should work without
requiring a subscription row in the database. This allows self-hosted
instances without Stripe to publish posts normally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 14:35:02 +01:00
Nevo David
0b7fd01a82 feat: urgent fix, stripe requires api key in the last version
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-03-06 19:37:08 +07:00
Nevo David
72e63a41c2 feat: fix sets extra space
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-03-06 11:20:57 +07:00
Nevo David
2a194b680e feat: telegram fix 2026-03-06 10:37:14 +07:00
Nevo David
9fa6786754 feat: remove lifetime check 2026-03-06 08:10:29 +07:00
Nevo David
624a4a9775 feat: show package 2026-03-05 15:39:31 +07:00
Nevo David
4ba1c5e415 feat: public controller modify sub 2026-03-05 15:00:32 +07:00
Nevo David
748e2190e9 feat: remove logic and add logic to enterprise 2026-03-05 12:03:06 +07:00
Nevo David
78b834d18f feat: enhance api 2026-03-05 10:41:18 +07:00
Nevo David
ad0b0259eb feat: add organization to auth flow 2026-03-05 09:40:52 +07:00
Nevo David
fe51c788c7 feat: agent media 2026-03-04 15:20:58 +07:00
Nevo David
24a37a56a7 Merge remote-tracking branch 'origin/main' 2026-03-04 13:08:55 +07:00
Nevo David
5d4808a07a feat: update x api 2026-03-04 13:08:39 +07:00
Enno Gelhaus
cdeef9b4d6
Fix email address for security vulnerability reporting 2026-03-03 15:03:25 +01:00
Nevo David
778d255a15 feat: url 2026-03-03 20:25:48 +07:00
Nevo David
52d84a2bfb feat: change redirect url 2026-03-03 19:42:43 +07:00
Nevo David
42d7927d1c feat: change redirect url 2026-03-03 19:22:47 +07:00
Nevo David
354b206280 feat: improve AppSumo webhook handling and auth safety
- Add activate event handler with org lookup via subscription or user/provider
- Add postRegistration guard in auth service to prevent errors on missing method
- Fix deactivate to use organization paymentId for subscription deletion
- Change Dependabot interval from daily to weekly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 18:54:08 +07:00
Nevo David
0d7e54023c feat: login change and appsumo 2026-03-03 17:42:17 +07:00
Nevo David
681e6d4cb4 feat: delete error 2026-03-03 15:25:48 +07:00
Nevo David
09341d345f feat: logout in billing screen 2026-03-03 13:15:54 +07:00
Nevo David
c36a5d54f2 Merge branch 'feat/mewe' 2026-03-03 11:09:11 +07:00
Nevo David
11e32c20fb feat: mcp with oauth2 2026-03-03 10:27:54 +07:00
Nevo David
72b0dfaf8a Merge remote-tracking branch 'origin/main' 2026-03-03 01:32:39 +07:00
Nevo David
e4c4d6470c feat: oauth 2026-03-03 01:32:26 +07:00
Nevo David
334dda7609 feat: oauth 2026-03-03 00:46:46 +07:00
Nevo David
aa37ad0a11
Add Virlo as a new sponsor in README
Added Virlo as a new sponsor with logo and description.
2026-03-02 23:33:22 +07:00
Nevo David
ff4ee6c5fe feat: maximum 500 2026-03-02 20:25:33 +07:00
Nevo David
98d9ddc83e feat: update stripe version 2026-03-02 17:12:54 +07:00
Nevo David
724e5bc497 feat: don't post without a subscription 2026-03-02 15:33:36 +07:00
Nevo David
16713f03b6 feat: facebook and instagram connect 2026-03-02 14:20:43 +07:00
Nevo David
daa1e0f91b feat: google my business pagination 2026-03-02 14:11:34 +07:00
Nevo David
5751a62f44 feat: multiple sses 2026-03-01 17:13:39 +07:00
Nevo David
d0a6ee330d feat: re-add sse option for mcp 2026-03-01 16:40:51 +07:00
Nevo David
6c39e810db feat: seperate mcp url 2026-02-28 16:53:03 +07:00
Nevo David
71833f3f39 feat: mewe 2026-02-28 15:01:54 +07:00
Nevo David
92abb07712 Merge branch 'main' into feat/mewe 2026-02-28 12:19:42 +07:00
Nevo David
ff89a21c98 feat: redis throttler 2026-02-27 19:41:20 +07:00
Nevo David
7bfa3e5ad9 feat: only success charges 2026-02-27 18:58:17 +07:00
Nevo David
258378770e feat: refund and cancellation 2026-02-27 18:37:29 +07:00
Nevo David
bd82f3e4ca feat: better tiktok errors 2026-02-27 10:40:32 +07:00
Nevo David
292f96f959 feat: collaborators fix 2026-02-22 16:53:15 +07:00
Nevo David
35f75e7ff5 feat: no collaborators in carusel 2026-02-22 16:36:22 +07:00
Nevo David
fdd7168742 feat: not possible to regsiter new attributes with tls 2026-02-22 00:10:25 +07:00
Nevo David
39dd1d4cdd feat: temporal tls and api key 2026-02-21 22:09:00 +07:00
Nevo David
7627b80bfc feat: remove upload throttle
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-02-20 17:53:13 +07:00
Nevo David
d7322d11fd feat: compression 2026-02-20 13:05:55 +07:00
Nevo David
b9f3b646fc feat: better indexes 2026-02-20 12:30:31 +07:00
Nevo David
5fddba6e98 feat: logout 2026-02-20 00:37:12 +07:00
Nevo David
ccd665715c feat: fix rate to only post request 2026-02-18 23:29:02 +07:00
Nevo David
b23298904a feat: tiktok fix 2026-02-18 21:07:12 +07:00
Nevo David
143bc229de feat: instagram multiple stories 2026-02-18 14:47:59 +07:00
Nevo David
001c70357c feat: postizpro 2026-02-18 08:34:26 +07:00
Nevo David
5d1988c2f1 fix emails and draft 2026-02-17 16:47:41 +07:00
Nevo David
118060bacd feat: tiktok 2026-02-17 14:07:13 +07:00
Nevo David
b744b60be6 feat: tiktok order 2026-02-17 11:32:49 +07:00
Nevo David
dfb5c05979 feat: mcp on route 2026-02-17 10:26:48 +07:00
Nevo David
13104962c6 feat: rotate api key 2026-02-17 10:05:12 +07:00
Nevo David
f7f5899d01 Feat: tiktok fixes 2026-02-16 20:32:54 +07:00
Nevo David
b854430f7d feat: release id must be string 2026-02-16 18:20:12 +07:00
Nevo David
009f2df074 feat: original name 2026-02-16 16:06:06 +07:00
Nevo David
613a4285ff feat: post missing modal 2026-02-16 00:17:59 +07:00
Nevo David
cfa52b2336 feat: bot nickname 2026-02-15 19:08:46 +07:00
Nevo David
381f9e8021 Merge remote-tracking branch 'origin/main' 2026-02-15 19:04:19 +07:00
Nevo David
16946b1c0f feat: fix reddit 2026-02-15 19:04:08 +07:00
Nevo David
9a7e051f5d
Merge pull request #1207 from cohax-llc/fix/upload-from-url-extension
fix(api): preserve file extension in upload-from-url endpoint
2026-02-15 16:37:07 +07:00
Nevo David
6f54180768 feat: same refresh token 2026-02-15 16:31:51 +07:00
Nevo David
38b1cb2ce5 feat: local store fixed when trying to download pictures
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-02-15 14:09:02 +07:00
Nevo David
ecb1b55ce1 feat: analytics public api 2026-02-15 12:40:02 +07:00
Nevo David
7ee7eacbcb Merge remote-tracking branch 'origin/main' 2026-02-15 11:41:54 +07:00
Nevo David
56efd49d8f feat: tiktok fixes 2026-02-15 11:41:39 +07:00
Enno Gelhaus
844449b378
Update checkout action version to v6 2026-02-14 18:59:40 +01:00
Nevo David
c908813d0f
Update README.md 2026-02-14 13:45:26 +07:00
Nevo David
dc8a380176 feat: README and SKILL 2026-02-14 11:57:12 +07:00
Nevo David
6a69ac7eb4 feat: skill 2026-02-14 11:42:56 +07:00
Nevo David
af08ae1187 feat: tools in public api 2026-02-14 11:00:14 +07:00
Nevo David
6aa6dfb614 feat: fix settings 2026-02-14 10:37:26 +07:00
Nevo David
2feadf4673 feat: platforms settings 2026-02-14 10:33:33 +07:00
Nevo David
2c475d1238 feat: postiz cli 2026-02-14 10:28:16 +07:00
Nevo David
33c8adbef1 feat: get settings 2026-02-14 10:27:02 +07:00
Nevo David
0c02554064 feat: cli 2026-02-14 09:48:10 +07:00
Nevo David
683d4c3682 feat: mewe 2026-02-14 09:32:26 +07:00
Nevo David
ba71bbf5a8 feat: cli agent 2026-02-13 22:41:21 +07:00
Nevo David
80d8804f5a feat: tiktok fixes 2026-02-13 21:49:08 +07:00
Nevo David
a098d612af feat: no posts with a deleted integration 2026-02-11 22:04:47 +07:00
Nevo David
3b57679430 feat: notifications 2026-02-11 21:05:56 +07:00
Nevo David
d929c96792 feat: map more errors, rate limits 2026-02-11 20:31:14 +07:00
Nevo David
7f7c336939 feat: restricted 2026-02-11 20:17:32 +07:00
Nevo David
d055e69335 feat: delete post 2026-02-11 17:41:13 +07:00
Nevo David
95956fe345 feat: throw wordpress 2026-02-11 16:57:55 +07:00
Nevo David
f7d36b36e7 feat: wordpress insufficent permission 2026-02-11 14:48:32 +07:00
Nevo David
82c8b53236 feat: fix bold and underline for gt and lt
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-02-11 13:29:11 +07:00
Nevo David
0ceaa3d04d feat: trial reels 2026-02-10 10:21:38 +07:00
Nevo David
5869300e64 feat: dont refresh 2026-02-09 18:50:13 +07:00
Nevo David
b3681d2ac1 feat: whop
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-02-09 17:02:28 +07:00
Nevo David
945dc09f48 feat: fix ai agent
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-02-09 12:42:36 +07:00
Nevo David
029ddb29d1 feat: fix linkedin carusel 2026-02-09 12:05:40 +07:00
Nevo David
5f5d746f8a feat: show file name in uploads 2026-02-09 11:37:22 +07:00
Nevo David
f5ff72b706 feat: platforms with a chrome extension
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-02-08 21:10:11 +07:00
Nevo David
e3c3854840 feat: platforms with a chrome extension 2026-02-08 20:53:28 +07:00
Nevo David
f72d63bd43 feat: update token to all orgs 2026-02-07 00:16:42 +07:00
Nevo David
1c3cb008c6 feat: retry resource 2026-02-06 23:49:11 +07:00
Emi Rexhepi
ee8b26f383 fix(api): preserve file extension in upload-from-url endpoint
The upload-from-url endpoint passed an empty originalname to
uploadFile(), causing extname('') to return '' and files to be saved
without an extension. Derive the extension from the response
Content-Type header (or URL fallback) so files are stored with the
correct extension on disk.

Fixes gitroomhq/postiz-app#1147

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 15:40:55 +01:00
Nevo David
52ec091400 feat: wordpress 2026-02-06 15:21:51 +07:00
Nevo David
9de380f6ba Merge remote-tracking branch 'origin/main' 2026-02-06 09:42:32 +07:00
Nevo David
c481add80b feat: switch org 2026-02-06 09:40:55 +07:00
Nevo David
572de8a2d0
Revise Postiz-as-a-service to Enterprise offering
Updated the Postiz-as-a-service section to reflect the enterprise offering and enhanced the description for clarity.
2026-02-05 20:48:29 +07:00
Nevo David
eccc767701 feat: fix transloadit 2026-02-05 14:15:35 +07:00
Nevo David
089b9ebc40 Merge remote-tracking branch 'origin/main' 2026-02-05 11:41:56 +07:00
Nevo David
650d92bb56 feat: allow mov 2026-02-05 11:41:42 +07:00
Nevo David
58bc35148c
Update README.md 2026-02-04 20:40:12 +07:00
Nevo David
5a193fd19e
Update README.md 2026-02-04 20:38:31 +07:00
Nevo David
0928f76284
Update README.md 2026-02-04 20:38:02 +07:00
Nevo David
c970bfab95 feat: hostinger 2026-02-04 20:36:46 +07:00
Nevo David
34243312ad feat: generate url 2026-02-04 15:43:22 +07:00
Nevo David
70be520c0b feat: public list integrations 2026-02-04 15:14:01 +07:00
Nevo David
e7999ea151 feat: apiKey 2026-02-04 13:01:10 +07:00
Nevo David
979b81d6d0 feat: enterprise 2026-02-04 12:32:44 +07:00
Nevo David
6d173ab7cd feat: enterprise 2026-02-04 12:32:00 +07:00
Nevo David
638debdaa4 feat: enterprise 2026-02-03 21:34:23 +07:00
Nevo David
f5ef80546e feat: enterprise 2026-02-03 21:12:28 +07:00
Nevo David
bb9e731174 feat: metadata 2026-02-02 23:01:33 +07:00
Nevo David
656f29eb34 feat: activate 2026-02-02 22:34:21 +07:00
Nevo David
437a3b5143 feat: tracking 2026-02-02 22:15:08 +07:00
Nevo David
47414acf29 feat: moltbook 2026-02-02 13:13:54 +07:00
Nevo David
68705fca69 feat: fix providers 2026-02-02 11:27:28 +07:00
Nevo David
a936da4851 feat: fix state for bluesky 2026-02-02 11:12:04 +07:00
Nevo David
1a8f6ff6ca fix: new error for facebook 2026-02-01 22:29:41 +07:00
Nevo David
9b712bb634 feat: independent provider adding 2026-02-01 19:41:07 +07:00
Nevo David
fe457573ec feat: independent provider adding
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-02-01 17:01:37 +07:00
Nevo David
5f830f11b1 feat: large json 2026-02-01 11:50:59 +07:00
Nevo David
e1c51effad feat: counter in global mode 2026-02-01 09:04:20 +07:00
Nevo David
980ea7c5b4 Merge remote-tracking branch 'origin/main' 2026-01-30 18:40:10 +07:00
Nevo David
42529ac438 feat: filters 2026-01-30 18:39:45 +07:00
Enno Gelhaus
4ed1ffe206
Update Node.js version in build workflow 2026-01-30 11:14:56 +01:00
Nevo David
ccce5d8122 feat: list view 2026-01-30 16:22:04 +07:00
Nevo David
7909228271 feat: better validation 2026-01-30 12:50:06 +07:00
Nevo David
17bde5858c feat: tiktok analytics 2026-01-30 12:23:28 +07:00
Nevo David
5c50e962ae feat: tiktok analytics 2026-01-30 11:06:50 +07:00
Nevo David
ee012fb021 fix: border image
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (20.17.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-01-28 18:44:35 +07:00
Nevo David
e551d151d6 fix: border image 2026-01-28 16:24:02 +07:00
Nevo David
881c37ed6f feat: streak 2026-01-28 16:00:48 +07:00
Nevo David
c3cdee5c21 feat: streak 2026-01-28 14:53:45 +07:00
Nevo David
aa17498a52 feat: easy close statistics 2026-01-28 10:53:39 +07:00
Nevo David
ddbfd16030 feat: faster remove of socials from scheduling 2026-01-28 10:50:32 +07:00
Nevo David
b6928138d2 feat: fix instagram analytics 2026-01-28 10:12:56 +07:00
Nevo David
078d4739c8 feat: fix instagram analytics 2026-01-28 09:58:57 +07:00
Nevo David
ec4b19c741 feat: delete tags 2026-01-27 22:17:25 +07:00
Nevo David
2813ed01f9 feat: protected reposting post when clicking update 2026-01-27 21:44:30 +07:00
Nevo David
defd9e0a63 feat: scheduling edge-cases 2026-01-27 18:13:02 +07:00
Nevo David
8c4e69dd3d feat: another safety to force termination of a workflow 2026-01-27 17:48:59 +07:00
Nevo David
7e92a767e4 remove focused channel 2026-01-26 12:28:50 +07:00
Nevo David
1d9af510e5 fix: tiktok 2026-01-26 11:51:37 +07:00
Nevo David
f39ab5dc80 fix: share post not only with community 2026-01-24 17:40:44 +07:00
Nevo David
ec5210be51 feat: add twitch 2026-01-24 16:35:54 +07:00
Nevo David
e9f2660002 feat: remove channel selection 2026-01-24 15:36:06 +07:00
Nevo David
7e1e5f36e3 feat: kick 2026-01-24 15:00:42 +07:00
Nevo David
bf9ab0fd95 youtube onboarding 2026-01-24 01:01:30 +07:00
Nevo David
8bb1fc826f upgrade upload images to v2 2026-01-23 18:42:35 +07:00
Nevo David
608185fd9a fix: onboarding 2026-01-23 17:06:46 +07:00
Nevo David
43ced2e7b6 feat: onboarding 2026-01-23 16:37:28 +07:00
Nevo David
ba7fc7a8e8 feat: onboarding 2026-01-23 16:20:28 +07:00
Nevo David
5cb69667b3 feat: drag and drop files 2026-01-23 13:01:09 +07:00
Nevo David
baea8b28ee feat: nostr instructions 2026-01-23 11:38:18 +07:00
Nevo David
9e54e315fd fix: settings on the global edit 2026-01-21 17:56:38 +07:00
Nevo David
98a6fb1013 feat: analytics changes 2026-01-21 16:01:02 +07:00
Nevo David
2346223a04 feat: fix facebook impressions 2026-01-21 15:54:37 +07:00
Nevo David
1e19868b36 Merge remote-tracking branch 'origin/main' 2026-01-21 15:12:51 +07:00
Nevo David
5cca81e002 feat: post analytics 2026-01-21 15:11:29 +07:00
Nevo David
bfa552401e
Add video link for Postiz features in README
Updated README to include a video link showcasing Postiz features.
2026-01-19 22:05:19 +07:00
Nevo David
99889d59e2 fix: autoplay video 2026-01-19 17:31:14 +07:00
Nevo David
c8f6cdb643 fix: z-index 2026-01-19 17:25:13 +07:00
Nevo David
5bad88daa9 feat: ask for shortlinking 2026-01-19 16:33:24 +07:00
Nevo David
d8b8b3d629 feat: maximize media 2026-01-19 16:08:17 +07:00
Nevo David
8a577dea73 feat: time table improvements 2026-01-19 14:55:07 +07:00
Nevo David
809476aa1d fix: postiz agent 2026-01-18 15:30:09 +07:00
Nevo David
0d5b45cd11 fix: statistics 2026-01-18 11:51:40 +07:00
Nevo David
604b029a9d feat: start button 2026-01-18 09:44:22 +07:00
Nevo David
87eca91b9b fix: urgent providers fix
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (20.17.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-01-17 17:01:54 +07:00
Nevo David
88e05853c1 fix: urgent providers fix 2026-01-17 16:55:43 +07:00
Nevo David
34b465aaa1 fix: urgent providers fix 2026-01-17 16:53:54 +07:00
Nevo David
005e3f753b fix: urgent providers fix 2026-01-17 16:49:40 +07:00
Nevo David
e29812501e fix: urgent providers fix 2026-01-17 16:35:56 +07:00
Nevo David
7eeb1cb044 fix: slack urgent fix for posting 2026-01-17 16:27:02 +07:00
Nevo David
5bfb97e9a4 fix: remove billing address 2026-01-17 13:53:54 +07:00
Nevo David
ba0a2498a4 fix: remove billing address 2026-01-17 13:28:09 +07:00
Nevo David
47e7c843c9 feat: better biling 2026-01-17 12:03:28 +07:00
Nevo David
ba4ad5deb2 feat: stripe changes 2026-01-17 11:42:12 +07:00
Nevo David
07b0c2e85d feat: refresh token before expiration for specific platforms 2026-01-16 20:41:29 +07:00
Nevo David
13d4bb0086 Merge remote-tracking branch 'origin/main' 2026-01-16 18:02:44 +07:00
Nevo David
abd73039c3 fix: support much smaller resolutions 2026-01-16 18:01:18 +07:00
Nevo David
0af727e0cf
Update README.md 2026-01-14 16:32:05 +07:00
Nevo David
563e7a257b fix: docker-compose for temporal
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (20.17.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-01-14 14:10:20 +07:00
Nevo David
d7a92e0b92 feat: safe stringify 2026-01-13 19:33:25 +07:00
Nevo David
5665774260 resend activation email 2026-01-12 18:55:24 +07:00
Nevo David
6859f0d049 feat: retry no activity 2026-01-12 16:15:39 +07:00
Nevo David
82d91fd8ed fix: don't modify gif 2026-01-12 15:54:12 +07:00
Nevo David
b2677649df fix: don't modify gif 2026-01-12 15:53:47 +07:00
Nevo David
fddf61f052 fix: logo 2026-01-11 19:30:21 +07:00
Nevo David
05b6b2ec00 fix: encode instead of decode 2026-01-11 18:22:18 +07:00
Nevo David
fca16b5c48 fix: rolling id 2026-01-11 16:39:45 +07:00
Nevo David
31ccf8d352 fix: rolling id 2026-01-11 15:46:41 +07:00
Nevo David
ce94f9bcc4 fix: dont try to post of parent is not null 2026-01-11 14:57:08 +07:00
Nevo David
c5bacf7d43 feat: change social concurrency 2026-01-11 13:50:54 +07:00
Nevo David
38cbeb8d95 feat: change social concurrency 2026-01-11 13:49:59 +07:00
Nevo David
46b1263304 feat: add comment from another user 2026-01-09 15:04:16 +07:00
Nevo David
631d7c15e8 feat: better workflow logic 2026-01-09 10:41:07 +07:00
Nevo David
3e8f5ddca6 fix: email notifiations 2026-01-08 21:44:43 +07:00
Nevo David
07238a06a6 feat: big workflow change 2026-01-08 21:26:29 +07:00
Nevo David
96661cc30d fix: url 2026-01-07 20:39:15 +07:00
Nevo David
774d29d798 feat: fetch post faster 2026-01-07 18:38:08 +07:00
Nevo David
bd24282d2b fix: don't wait for workflow start, for faster creation 2026-01-07 15:40:31 +07:00
Nevo David
82a92afabb fix: don't wait for workflow start, for faster creation 2026-01-07 15:31:51 +07:00
Nevo David
2cc639bbfc fix: workflow termination 2026-01-07 13:57:45 +07:00
Nevo David
2544e870aa feat: delayed comments 2026-01-07 12:29:22 +07:00
Nevo David
4836b2e7c1 feat: delay 2026-01-07 11:35:48 +07:00
Nevo David
407cfa67ef fix: priority fix for send email 2026-01-07 09:59:33 +07:00
Nevo David
7e73017d3f fix: priority fix for send email 2026-01-07 09:54:45 +07:00
Nevo David
bddb937258 feat: longer digest 2026-01-07 01:25:42 +07:00
Nevo David
7001295fa5 feat: longer digest 2026-01-07 01:22:02 +07:00
Nevo David
475cf8e0fb feat: longer digest 2026-01-07 01:21:11 +07:00
Nevo David
0f4c39ede0 feat: never exit workflow 2026-01-06 21:13:51 +07:00
Nevo David
012a347ed6 feat: logging error 2026-01-06 20:54:55 +07:00
Nevo David
633c08fde7 feat: try await in send email 2026-01-06 20:37:28 +07:00
Nevo David
8b0fe19b41 feat: change cancellation type 2026-01-06 20:04:56 +07:00
Nevo David
95eace849b fix: send email 2026-01-06 17:02:50 +07:00
Nevo David
5f1a77a3b0 fix: use existing workflow 2026-01-06 16:35:42 +07:00
Nevo David
7ac99e8259 fix: digest send async 2026-01-06 12:51:06 +07:00
Nevo David
5d76475801 fix: 50ms between emails, prevent resend from crashing 2026-01-06 12:34:54 +07:00
Nevo David
bb9aa1aee6 feat: fix post edit 2026-01-05 22:34:36 +07:00
Nevo David
ead98cb41f fix: no refresh - set error 2026-01-05 22:17:51 +07:00
Nevo David
fc349942b5 feat: fix refresh token 2026-01-05 21:30:04 +07:00
Nevo David
c7b83ee361 Merge remote-tracking branch 'origin/main' 2026-01-05 21:10:27 +07:00
Nevo David
3f674de401 fix: old providers without selection 2026-01-05 21:10:12 +07:00
Enno Gelhaus
de74191bab feat: add spotlight integration to Sentry initialization 2026-01-05 14:29:16 +01:00
Nevo David
691d15db03 fix: youtube and gmb 2026-01-05 18:54:11 +07:00
Nevo David
dac6e8e189 fix: refresh token 2026-01-05 18:50:25 +07:00
Nevo David
204739f049 feat: fix refresh token 2026-01-05 18:38:17 +07:00
Nevo David
0144e39841 feat: no sleep option 2026-01-05 18:00:49 +07:00
Nevo David
1291937cb2 fix: only queue state 2026-01-05 17:58:21 +07:00
Nevo David
b5979b290d feat: run old also 2026-01-05 17:55:02 +07:00
Nevo David
6eb55e128f fix: run prod 2026-01-05 17:49:11 +07:00
Nevo David
78e1a14388 feat: redeploy 2026-01-05 17:41:04 +07:00
Nevo David
c740da360b feat: fix lockfile 2026-01-05 17:38:46 +07:00
Nevo David
95303b2975
Merge pull request #1157 from gitroomhq/feat/temporal
Moving from BullMQ to Temporal - big change
2026-01-05 17:35:14 +07:00
Nevo David
6633fab924 feat: final temporal touches 2026-01-05 17:28:53 +07:00
Nevo David
da0045428a feat: temporal - huge refactor 2026-01-05 15:49:19 +07:00
Nevo David
d8a6215155 feat: before split 2026-01-05 11:35:15 +07:00
Nevo David
9e0eff7e5a feat: payload wizard 2026-01-03 11:00:06 +07:00
Nevo David
cd085e27ca feat: more translations 2026-01-01 16:07:58 +07:00
Nevo David
b3d298daff fix: change vietnam flag 2026-01-01 15:09:23 +07:00
Nevo David
4f0f4c27f9 Merge remote-tracking branch 'origin/main' 2026-01-01 14:56:04 +07:00
Nevo David
c8c812e1eb feat: fix additional settings modal 2026-01-01 14:55:41 +07:00
Nevo David
44f190d466
Update README.md 2025-12-31 13:03:41 +07:00
Nevo David
5f4de7c987 feat: fullscreen 2025-12-29 22:20:16 +07:00
Nevo David
faef57738c feat: generator 2025-12-29 22:01:47 +07:00
Nevo David
0e5d6f8826 fix: hot fix for modal creation
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (20.17.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2025-12-29 09:36:00 +07:00
Nevo David
6729de4563 fix: hot fix for modal creation 2025-12-29 09:31:13 +07:00
Nevo David
ca941aed85 fix: customer 2025-12-29 02:59:17 +07:00
Nevo David
00caa7ddc4 feat: moving to dub partners 2025-12-29 02:11:40 +07:00
Nevo David
e5a4c558dd feat: fix throttle 2025-12-28 21:01:20 +07:00
Nevo David
b9062423a4 feat: fix linkedin pdf 2025-12-28 18:54:31 +07:00
Nevo David
7a82798cdc feat: fix throttler 2025-12-28 18:24:33 +07:00
Nevo David
3387b3c989 feat: higher concurrency rate for facebook 2025-12-28 18:12:18 +07:00
Nevo David
e0b496c4f9 feat: settings change 2025-12-28 17:53:45 +07:00
Nevo David
5f945bd396 feat: color changes 2025-12-28 17:37:00 +07:00
Nevo David
a188baa6f7 feat: calendar skeleton 2025-12-28 16:23:40 +07:00
Nevo David
9c751fb725 feat: provider checks protection 2025-12-28 10:28:09 +07:00
Nevo David
8c1191a093 fix: full screen settings 2025-12-28 01:36:41 +07:00
Nevo David
2cf0d632de feat: editor bug fixes, some previews 2025-12-27 23:30:15 +07:00
Nevo David
0d134c0a6f feat: editor bug fixes, some previews 2025-12-27 23:20:07 +07:00
Nevo David
90da9d4aff fix: pagination 2025-12-27 10:24:11 +07:00
Nevo David
6f889d42c8 feat: responsive payment 2025-12-26 19:43:55 +07:00
Nevo David
f98ae083ab feat: fix monthly yearly 2025-12-26 18:14:32 +07:00
Nevo David
1c61e76c9f fix: better block view 2025-12-26 17:44:10 +07:00
Nevo David
70d07249d3 faet: remove comments 2025-12-26 14:11:24 +07:00
Nevo David
13137e803d Merge remote-tracking branch 'origin/main' 2025-12-26 11:21:53 +07:00
Nevo David
1da332eec1 feat: sentry errors 2025-12-26 11:21:12 +07:00
Enno Gelhaus
0043029292
Merge pull request #1138 from Fer-r/feature/webp-image-upload-support
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (20.17.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
feat: add WebP image upload support
2025-12-26 04:54:45 +01:00
fer-r
2497bd139d feat: add WebP image upload support
- Added image/webp to the list of allowed MIME types in the file uploader preprocessor
- Added .webp extension validation in ValidUrlExtension validator
- Updated error message to include .webp in the list of valid extensions

Closes #1055
2025-12-26 03:10:49 +01:00
Nevo David
d4f18d1030 HOT FIX
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (20.17.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2025-12-26 08:09:16 +07:00
Nevo David
74b84b5eeb feat: fix information display 2025-12-25 21:38:08 +07:00
Nevo David
f5dff1e655 feat: svg change
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (20.17.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2025-12-25 19:02:52 +07:00
Nevo David
37c45743a8 feat: refactor for creation modal 2025-12-25 18:29:12 +07:00
Nevo David
ec623a678e fix: discord support 2025-12-22 23:44:03 +07:00
Nevo David
63c3e1c233 fix: tolt 2025-12-22 21:15:14 +07:00
Nevo David
f4dec2ed26 feat: powered by 2025-12-22 19:17:10 +07:00
Nevo David
21f0b64344 feat: trailing 2025-12-22 19:02:50 +07:00
Nevo David
0690d0b80b feat: logo fix 2025-12-22 18:33:23 +07:00
Nevo David
d43690e688 feat: force nextjs version 2025-12-22 18:28:34 +07:00
Nevo David
eaec9bf585 feat: neyner fix 2025-12-22 18:04:36 +07:00
Nevo David
8d0b4a125a feat: upgrade old packages 2025-12-22 17:51:27 +07:00
Nevo David
3e04076d5d feat: check payment 2025-12-22 17:42:40 +07:00
Nevo David
9c2abfb47f feat: nextjs upgrade 2025-12-22 17:36:23 +07:00
Nevo David
1114bbd8df feat: old layout remove 2025-12-22 17:29:18 +07:00
Nevo David
59d0df5b5c feat: package upgrade 2025-12-22 17:24:14 +07:00
Nevo David
e6f2d87943 feat: extension 2025-12-22 17:15:49 +07:00
Nevo David
0fe10976b0 feat: billing change 2025-12-22 17:08:35 +07:00
Nevo David
991d354c34 feat: billing change 2025-12-22 16:55:51 +07:00
Nevo David
022f9bf8c6 feat: width change 2025-12-19 10:56:50 +07:00
Nevo David
b7ce23cc5d fix: mobile cons 2025-12-18 14:38:40 +07:00
Nevo David
e066804178 Merge remote-tracking branch 'origin/main' 2025-12-18 14:28:16 +07:00
Nevo David
7294cad60b feat: branded signin-signup 2025-12-18 14:27:57 +07:00
Nevo David
f4ce7f74f9 feat: register change 2025-12-18 12:15:04 +07:00
Enno Gelhaus
41090bdab8
Merge pull request #1118 from adambkovacs/fix/docker-build-fresh-base-image 2025-12-15 13:52:03 +01:00
Adam Kovacs
6c4cf4164c fix(ci): Force fresh base image in Docker builds
Add --pull and --no-cache flags to docker buildx build to ensure:
- Base image (node:22.20-alpine) is always pulled fresh
- No cached layers from previous builds are used

This fixes issue #1079 where published images contained outdated
Node.js v20.18.x instead of the expected v22.20.x from Dockerfile.dev,
causing Prisma 7 compatibility failures.

Fixes #1079
2025-12-14 21:21:04 -08:00
Enno Gelhaus
8e30271fbc
Temporarily disable ESLint 2025-12-09 16:00:17 +01:00
Nevo David
1f7279719c Merge remote-tracking branch 'origin/main' 2025-12-09 21:36:32 +07:00
Nevo David
7ae9ddbd02 feat: no follow in terms and privacy 2025-12-09 21:36:16 +07:00
Enno Gelhaus
aa35492fb7
Update cache dependency path to pnpm-lock.yaml 2025-12-06 23:17:02 +01:00
Enno Gelhaus
ce3cd7ba0e
Change cache dependency path to pnpm-lock.json
Updated cache dependency path to use pnpm-lock.json instead of package-lock.json.
2025-12-06 23:15:17 +01:00
Enno Gelhaus
f7aa06475c
Refactor build workflow by removing unnecessary steps
Removed the SonarQube analysis step and commit SHA retrieval from the build workflow.
2025-12-06 23:14:38 +01:00
Enno Gelhaus
6304354953
feat/github CI cleanup 2025-12-06 23:08:29 +01:00
Enno Gelhaus
10d807430b
feat/dependabot 2025-12-06 23:06:13 +01:00
Enno Gelhaus
4ac671f646
feat/Github Issue templates 2025-12-06 22:46:41 +01:00
Nevo David
16d4b4508f feat: fix nostr 2025-12-05 23:08:27 +07:00
Nevo David
ab2502086a fix: nostr 2025-12-05 21:29:57 +07:00
Nevo David
2706d511ce fix: hot fix for menu items
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2025-12-05 15:55:14 +07:00
Nevo David
c982e30e96 feat: notifcations settings
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2025-12-05 14:32:20 +07:00
Nevo David
d89eb44b8f feat: ui fixes 2025-12-05 11:39:00 +07:00
Nevo David
2b32d561e9 feat: fix z-index 2025-12-04 19:13:43 +07:00
Nevo David
45294e07b5 feat: better layout 2025-12-04 18:16:50 +07:00
Nevo David
943acec8e4 fix: refresh token unified, and found some bugs 2025-12-04 14:12:33 +07:00
600 changed files with 54395 additions and 30942 deletions

View file

@ -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=""

View file

@ -1,19 +0,0 @@
name: "🙏🏻 Installation Problem"
description: "Report an issue with installation"
title: "Installation Problem"
labels: ["type: installation"]
body:
- type: markdown
attributes:
value: For installation issues, please visit our [Discord Support](https://discord.postiz.com) for assistance.
- type: textarea
id: feature-description
validations:
required: true
attributes:
label: For installation issues, please visit our https://discord.postiz.com for assistance.
description: For installation issues, please visit our [Discord Support](https://discord.postiz.com) for assistance.
placeholder: |
For installation issues, please visit our https://discord.postiz.com for assistance.
Please do not save this issue - do not submit installation issues on GitHub.

14
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1,14 @@
# Disable the default option to open a blank issue
blank_issues_enabled: true
# Define your custom links
contact_links:
# The first link definition
- name: 🙏 Installation Issue
url: https://discord.postiz.com
about: If you have an installation / configuration issue.
# You can add more links if needed
- name: Security Issue
url: https://github.com/gitroomhq/postiz-app/security/advisories/new
about: Please submit security Issues our GitHub Security Advisories.

View file

@ -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

BIN
.github/sponsors/hostinger.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View file

@ -1,69 +0,0 @@
---
name: Build
on:
push:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: ['20.17.0']
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: |
${{ env.STORE_PATH }}
${{ github.workspace }}/.next/cache
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
restore-keys: |
${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm run build
- name: Get Commit SHA (short)
id: get_version
run: |
# Get the short 8-character commit SHA
VERSION=$(git rev-parse --short=8 HEAD)
echo "Commit SHA is $VERSION"
echo "tag=$VERSION" >> $GITHUB_OUTPUT
- name: SonarQube Analysis (Branch)
uses: SonarSource/sonarqube-scan-action@v6
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
with:
args: >
-Dsonar.projectVersion=${{ steps.get_version.outputs.tag }}

View file

@ -53,6 +53,8 @@ jobs:
-f Dockerfile.dev \
-t ghcr.io/gitroomhq/postiz-app:${{ env.CONTAINERVER }}-${{ matrix.arch }} \
--build-arg NEXT_PUBLIC_VERSION=${{ env.NEXT_PUBLIC_VERSION }} \
--pull \
--no-cache \
--provenance=false --sbom=false \
--output "type=registry,name=ghcr.io/gitroomhq/postiz-app:${{ env.CONTAINERVER }}-${{ matrix.arch }}" .

View file

@ -1,71 +0,0 @@
---
name: Build
on:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: ['20.17.0']
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
environment:
name: build-pr
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: |
${{ env.STORE_PATH }}
${{ github.workspace }}/.next/cache
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
restore-keys: |
${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm run build
- name: Get Commit SHA (short)
id: get_version
run: |
# Get the short 8-character commit SHA
VERSION=$(git rev-parse --short=8 HEAD)
echo "Commit SHA is $VERSION"
echo "tag=$VERSION" >> $GITHUB_OUTPUT
- name: SonarQube Analysis (Pull Request)
uses: SonarSource/sonarqube-scan-action@v6
with:
args: >
-Dsonar.projectVersion=${{ steps.get_version.outputs.tag }}
-Dsonar.pullrequest.key=${{ github.event.pull_request.number }}
-Dsonar.pullrequest.branch=${{ github.event.pull_request.head.ref }}
-Dsonar.pullrequest.base=${{ github.event.pull_request.base.ref }}

54
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,54 @@
---
name: Build
on:
push:
merge_group:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: ['22.12.0']
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 10
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
# - name: Setup pnpm cache
# uses: actions/cache@v4
# with:
# path: |
# ${{ env.STORE_PATH }}
# ${{ github.workspace }}/.next/cache
# key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
# restore-keys: |
# ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm run build

View file

@ -1,14 +1,16 @@
---
name: "Code Quality Analysis"
name: "Code Quality Analysis"
on:
push:
branches:
- dev1
- main
paths:
- apps/**
- '!apps/docs/**'
- libraries/**
merge_group:
jobs:
analyze:

View file

@ -39,7 +39,7 @@ jobs:
node-version: '20'
cache: 'npm'
cache-dependency-path: |
**/package-lock.json
**/pnpm-lock.yaml
- name: Install ESLint
run: |

View file

@ -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
View file

@ -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
View 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.

View file

@ -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:

View file

@ -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

View file

@ -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 @@
| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| ![Image 3](https://github.com/user-attachments/assets/d51786ee-ddd8-4ef8-8138-5192e9cfe7c3) | ![Image 4](https://github.com/user-attachments/assets/91f83c89-22f6-43d6-b7aa-d2d3378289fb) |
### 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
[![Star History Chart](https://api.star-history.com/svg?repos=gitroomhq/postiz-app&type=Date)](https://www.star-history.com/#gitroomhq/postiz-app&Date)
[![Star History Chart](https://api.star-history.com/svg?repos=gitroomhq/postiz-app&type=date&legend=top-left)](https://www.star-history.com/#gitroomhq/postiz-app&type=date&legend=top-left)
## License

View file

@ -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.

View file

@ -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];

View 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();
}
}

View file

@ -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 getAgencyByUsers(@GetUserFromRequest() user: User) {
return (await this._agenciesService.getAgencyByUser(user)) || {};
}
@Post('/')
async createAgency(
@GetUserFromRequest() user: User,
@Body() body: CreateAgencyDto
) {
return this._agenciesService.createAgency(user, body);
}
@Post('/action/:action/:id')
async updateAgency(
@GetUserFromRequest() user: User,
@Param('action') action: string,
@Param('id') id: string
) {
if (!user.isSuperAdmin) {
return 400;
}
return this._agenciesService.approveOrDecline(user.email, action, id);
}
}

View file

@ -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);
}
}

View 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);
}
}

View 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);
}
}

View file

@ -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 });

View file

@ -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);
}
}

View file

@ -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';
@ -62,6 +62,23 @@ export class BillingController {
};
}
@Post('/embedded')
embedded(
@GetOrgFromRequest() org: Organization,
@GetUserFromRequest() user: User,
@Body() body: BillingSubscribeDto,
@Req() req: Request
) {
const uniqueId = req?.cookies?.track;
return this._stripeService.embedded(
uniqueId,
org.id,
user.id,
body,
org.allowTrial
);
}
@Post('/subscribe')
subscribe(
@GetOrgFromRequest() org: Organization,
@ -127,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 },

View file

@ -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 {

View 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 };
}
}
}

View file

@ -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,23 +18,20 @@ 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,
} from '@gitroom/backend/services/auth/permissions/permission.exception.class';
import { uniqBy } from 'lodash';
import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service';
@ApiTags('Integrations')
@Controller('/integrations')
@ -45,11 +39,18 @@ export class IntegrationsController {
constructor(
private _integrationManager: IntegrationManager,
private _integrationService: IntegrationService,
private _postService: PostsService
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')
@ -100,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,
@ -193,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
@ -222,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 };
@ -338,37 +352,24 @@ export class IntegrationsController {
return load;
} catch (err) {
if (err instanceof RefreshToken) {
const { accessToken, refreshToken, expiresIn, additionalSettings } =
await integrationProvider.refreshToken(getIntegration.refreshToken);
const data = await this._refreshIntegrationService.refresh(
getIntegration
);
if (!data) {
return;
}
const { accessToken } = data;
if (accessToken) {
await this._integrationService.createOrUpdateIntegration(
additionalSettings,
!!integrationProvider.oneTimeToken,
getIntegration.organizationId,
getIntegration.name,
getIntegration.picture!,
'social',
getIntegration.internalId,
getIntegration.providerIdentifier,
accessToken,
refreshToken,
expiresIn
);
getIntegration.token = accessToken;
if (integrationProvider.refreshWait) {
await timer(10000);
}
return this.functionIntegration(org, body);
} else {
await this._integrationService.disconnectChannel(
org.id,
getIntegration
);
return false;
}
return false;
}
return false;
@ -377,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);
}
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,
@ -533,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,
@ -566,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) => {});
}
}
@ -608,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 };
}
}
}

View file

@ -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;
}
}

View file

@ -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')

View file

@ -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
);
}
}

View file

@ -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.`,
};
}
}

View 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 };
}
}

View 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);
}
}

View 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() };
}
}

View file

@ -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')

View file

@ -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) {}
}
}

View file

@ -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
);
}
}

View file

@ -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);
}
}

View file

@ -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,

View file

@ -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';
@ -22,6 +23,7 @@ import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions
import { ApiTags } from '@nestjs/swagger';
import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service';
import { UserDetailDto } from '@gitroom/nestjs-libraries/dtos/users/user.details.dto';
import { EmailNotificationsDto } from '@gitroom/nestjs-libraries/dtos/users/email-notifications.dto';
import { HttpForbiddenException } from '@gitroom/nestjs-libraries/services/exception.filter';
import { RealIP } from 'nestjs-real-ip';
import { UserAgent } from '@gitroom/nestjs-libraries/user/user.agent';
@ -41,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,
@ -68,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);
}
@ -125,6 +145,25 @@ export class UsersController {
return this._userService.changePersonal(user.id, body);
}
@Get('/email-notifications')
async getEmailNotifications(@GetUserFromRequest() user: User) {
return this._userService.getEmailNotifications(user.id);
}
@Post('/email-notifications')
async updateEmailNotifications(
@GetUserFromRequest() user: User,
@Body() body: EmailNotificationsDto
) {
return this._userService.updateEmailNotifications(user.id, body);
}
@Post('/api-key/rotate')
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
async rotateApiKey(@GetOrgFromRequest() organization: Organization) {
return this._orgService.updateApiKey(organization.id);
}
@Get('/subscription')
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
async getSubscription(@GetOrgFromRequest() organization: Organization) {

View file

@ -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' },

View file

@ -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,

View file

@ -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.

View file

@ -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);
}
}
}
}

View file

@ -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);
}
}

View file

@ -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)

View file

@ -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;

View file

@ -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
);
};
}

View file

@ -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),

View file

@ -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',

View file

@ -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!,
};
}
}

View file

@ -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',

View file

@ -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();
}
}
}

View file

@ -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 });
}
}

View file

@ -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()
);

View file

@ -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();
}

View file

@ -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];
},

View file

@ -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;
}
}

View file

@ -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"
}

View file

@ -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 {}

View file

@ -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 start() {
// some comment again
await NestFactory.createApplicationContext(CronModule);
}
start();

View file

@ -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'),
},
});
}
}
}

View file

@ -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,
},
});
}
}
}

View file

@ -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
View 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/*"
]
}
}

View file

@ -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/*"
]
}
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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

View file

@ -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

View file

@ -1,13 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@theme {
--animate-spin-slow: spin 20s linear infinite;
@keyframes spin {
to {
transform: rotate(360deg);
}
}
}

View 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;
}
);

View file

@ -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;
}

View file

@ -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"
}
}

View file

@ -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;
});

View file

@ -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>
);
});

View file

@ -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 />);

View file

@ -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 az
const alphabet = 'abcdefghijklmnopqrstuvwxyz';
let result = '';
while (result.length < length) {
result = alphabet[hash % 26] + result;
hash = Math.floor(hash / 26);
}
return result;
}

View file

@ -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;
}

View file

@ -1,8 +0,0 @@
.container {
width: 100%;
height: 50vh;
font-size: 2rem;
display: flex;
align-items: center;
justify-content: center;
}

View file

@ -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>;
}

View file

@ -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>

View file

@ -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();

View file

@ -1,7 +0,0 @@
body {
background-color: #242424;
}
.container {
color: #ffffff;
}

View file

@ -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>
);
}

View file

@ -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>

View file

@ -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();

View file

@ -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>
);
}

View file

@ -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;
}

Some files were not shown because too many files have changed in this diff Show more