Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
178619d
fix(integrations): send a single notification when a token refresh fails
giladresisi Jul 6, 2026
2e80793
feat(instagram): use custom thumbnail as reel cover via cover_url
giladresisi Aug 7, 2026
4f469c7
fix(providers): wire the LinkedIn and Moltbook settings DTOs
giladresisi Aug 12, 2026
e5f6fff
feat(mcp): accept { path, thumbnail } attachments for video covers
giladresisi Aug 19, 2026
78515f7
fix(uploads): honor HTTP Range requests so local-storage video upload…
giladresisi Sep 17, 2026
c38f8ed
chore(deps): pin the Sentry SDK to 10.56.0 to pick up the span/scope …
giladresisi Sep 18, 2026
c0bd447
fix(frontend): report React render crashes to Sentry from the crash s…
giladresisi Sep 18, 2026
d727ac0
fix(frontend): keep the 402 response readable after the payment dialog
giladresisi Sep 10, 2026
971aa36
fix(frontend): tolerate a non-JSON 402 body in the payment dialog
giladresisi Sep 10, 2026
4b933cd
feat(facebook): optional title for feed video posts
giladresisi Sep 26, 2026
cd48401
feat: canva intent
nevo-david Sep 28, 2026
c17f0c2
Merge remote-tracking branch 'origin/main'
nevo-david Sep 28, 2026
7229c33
feat: schema fix
nevo-david Sep 28, 2026
1752382
feat: force token 200
nevo-david Sep 28, 2026
c033aff
fix(frontend): stay on Add Channel when the 402 dialog is cancelled
giladresisi Sep 28, 2026
b7e3df3
fix(frontend): stop the web3, custom-fields and extension connect flo…
giladresisi Sep 28, 2026
f67d2fd
Merge branch 'main' into fix/after-request-402-body-read-main
giladresisi Sep 28, 2026
73a0dea
Merge pull request #2159 from gitroomhq/fix/after-request-402-body-re…
giladresisi Sep 28, 2026
9dbebc2
fix(frontend): don't report a failed post save as successful
giladresisi Sep 28, 2026
cab42b1
fix(frontend): stop the Sentry report dialog on the Chrome wording of…
giladresisi Sep 28, 2026
f0f23f4
Merge pull request #2161 from gitroomhq/fix/post-save-no-success-on-e…
giladresisi Sep 28, 2026
aaa4343
Merge branch 'main' into fix/sentry-ignore-uppy-progress-chrome
giladresisi Sep 28, 2026
6562791
fix(uploads): clamp an over-long Range end instead of answering 416
giladresisi Sep 28, 2026
2318153
Merge pull request #2095 from gitroomhq/fix/local-uploads-range-main
giladresisi Sep 28, 2026
935e069
Merge pull request #2162 from gitroomhq/fix/sentry-ignore-uppy-progre…
giladresisi Sep 28, 2026
29478ed
fix(media): keep the rest of the upload batch when one file fails
giladresisi Sep 28, 2026
3b36d5e
chore(i18n): translate the partial upload failure toast
giladresisi Sep 28, 2026
4db23eb
Merge pull request #2163 from gitroomhq/fix/uploader-keep-batch-on-fi…
giladresisi Sep 28, 2026
a4314ea
fix(frontend): remove duplicate 'use client' directive in VK provider
giladresisi Sep 28, 2026
528f6ea
Merge pull request #2164 from gitroomhq/fix/vk-duplicate-use-client
giladresisi Sep 28, 2026
3f21703
fix(telegram): stop logging the post text on publish
giladresisi Sep 11, 2026
d934731
fix(facebook): retry the temporary posting-rate block (368 / 1390008)…
giladresisi Sep 11, 2026
480ee7e
fix(editor): stop wiping in-flight uploads when an earlier upload fin…
giladresisi Sep 29, 2026
852acde
Merge pull request #2171 from gitroomhq/fix/editor-uploader-keep-infl…
giladresisi Sep 29, 2026
b0103f7
fix(wordpress): send a Postiz User-Agent on requests to the WordPress…
giladresisi Sep 29, 2026
371d8ec
Merge pull request #2172 from gitroomhq/fix/wordpress-user-agent
giladresisi Sep 29, 2026
7abbdde
Merge pull request #2155 from gitroomhq/feat/facebook-video-title
giladresisi Sep 29, 2026
881ac76
fix(tiktok): send video_cover_timestamp_ms on Direct Post video init
giladresisi Aug 30, 2026
148cc96
fix(tiktok): send a 0ms cover timestamp explicitly
giladresisi Sep 29, 2026
716544f
Merge branch 'main' into feat/ig-reel-cover-image
giladresisi Sep 29, 2026
72d6015
Merge pull request #2174 from gitroomhq/feat/tiktok-video-cover-main
giladresisi Sep 29, 2026
3205ce1
Revert "feat(mcp): accept { path, thumbnail } attachments for video c…
giladresisi Sep 29, 2026
d739d08
Merge pull request #2102 from gitroomhq/fix/global-error-sentry-report
giladresisi Sep 29, 2026
502a6f8
Merge remote-tracking branch 'origin/main' into fix/wire-settings-dtos
giladresisi Sep 29, 2026
8e09aeb
fix(moltbook): keep submolt optional in the settings DTO
giladresisi Sep 29, 2026
5f2f39d
Merge branch 'main' into fix/wire-settings-dtos
giladresisi Sep 29, 2026
235e7da
Merge pull request #2166 from gitroomhq/fix/telegram-drop-post-text-l…
giladresisi Sep 29, 2026
3cccf2f
Merge pull request #2167 from gitroomhq/fix/facebook-368-rate-limit-r…
giladresisi Sep 29, 2026
de396df
Merge pull request #1880 from gitroomhq/fix/wire-settings-dtos
giladresisi Sep 29, 2026
b2220ae
Merge pull request #1855 from gitroomhq/feat/ig-reel-cover-image
giladresisi Sep 29, 2026
2e1b6eb
feat(sponsors): add rapidproxy to readme
egelhaus Sep 29, 2026
2ebd93e
fix(frontend): close the edit modal instead of crashing when the post…
giladresisi Sep 29, 2026
22f266a
fix(media): fail the upload step when the backend returns an error
giladresisi Sep 29, 2026
6eceb7e
Merge pull request #2177 from gitroomhq/fix/uploader-failed-completion
giladresisi Sep 29, 2026
9efa1f5
Merge pull request #2178 from gitroomhq/fix/edit-modal-missing-channel
giladresisi Sep 29, 2026
9c4524e
Merge pull request #2101 from gitroomhq/chore/sentry-sdk-10.56.0
nevo-david Sep 30, 2026
2800807
redeploy
nevo-david Sep 30, 2026
34509af
Merge remote-tracking branch 'origin/main'
nevo-david Sep 30, 2026
538f3e6
fix(pinterest): map the "not permitted to access that resource" pin e…
giladresisi Sep 30, 2026
852ac51
feat: more info
nevo-david Sep 30, 2026
b5ba8c8
Merge pull request #2179 from gitroomhq/fix/pinterest-not-permitted-m…
giladresisi Sep 30, 2026
da7f7a1
fix(tiktok): resolve the public post id for FILE_UPLOAD publish ids i…
giladresisi Sep 30, 2026
3f83306
Merge pull request #2180 from gitroomhq/fix/tiktok-analytics-file-pub…
giladresisi Sep 30, 2026
634c6da
fix(tiktok): keep the full public post id instead of the JSON.parse-r…
giladresisi Sep 30, 2026
33d33d1
Merge pull request #2182 from gitroomhq/fix/tiktok-post-id-precision
giladresisi Sep 30, 2026
80dacd7
feat(groups): rename a channel group from the channel menu
giladresisi Sep 1, 2026
c43916d
fix(groups): settle the rename promise when the modal is dismissed
giladresisi Sep 30, 2026
9cc6bf2
Merge branch 'main' into feat/edit-group-name-main
giladresisi Sep 30, 2026
496c380
Merge pull request #2183 from gitroomhq/feat/edit-group-name-main
giladresisi Sep 30, 2026
ff561b2
Merge pull request #1678 from gitroomhq/fix/duplicate-refresh-error-e…
giladresisi Sep 30, 2026
cc0fdf9
docs(claude): add shared-query and persisted-data change rules
giladresisi Aug 28, 2026
2a46d88
Merge pull request #2184 from gitroomhq/chore/claude-md-shared-data-r…
giladresisi Sep 30, 2026
bcbc119
fix(tiktok): resolve and persist the public post id for analytics
giladresisi Sep 2, 2026
4637a51
fix(tiktok): resolve photo publish ids too
giladresisi Sep 2, 2026
2407718
docs(claude): add a rule on credentials and sensitive fields in respo…
giladresisi Oct 1, 2026
e0a08a7
Merge pull request #2191 from gitroomhq/docs/claude-md-no-credential-…
giladresisi Oct 1, 2026
b6ead58
fix(media): bound media reads that stall after their headers
giladresisi Oct 1, 2026
a2794ab
fix(media): read media through the SSRF-safe axios client
giladresisi Oct 1, 2026
9fedac4
feat: responsive
nevo-david Oct 2, 2026
9554e9f
fix(media): fail a stalled media read instead of repeating the publish
giladresisi Oct 2, 2026
f5564d9
fix(media): give a media read two minutes before calling it stalled
giladresisi Oct 2, 2026
11d24f3
Merge pull request #2193 from gitroomhq/fix/media-read-timeout
giladresisi Oct 2, 2026
ca07a51
refactor(posts): extract release resolution into resolveRelease
giladresisi Oct 2, 2026
9c697e7
Merge branch 'main' into fix/tiktok-persist-post-id-main
giladresisi Oct 2, 2026
56f42d0
Merge pull request #2186 from gitroomhq/fix/tiktok-persist-post-id-main
giladresisi Oct 2, 2026
876ab9e
feat: self hosting connector
nevo-david Oct 2, 2026
8e42f09
Merge remote-tracking branch 'origin/main'
nevo-david Oct 2, 2026
28e23d9
merge: upstream main / v2.25.0 (374fb202..8e42f09c, 87 commits)
JOY Oct 2, 2026
d1e4960
fix(sync): brand the self-hosted relay MCP servers as Crove MCP
JOY Oct 2, 2026
1a302e3
Merge pull request #67 from DOS/upstream-sync-v2.25.0
JOY Oct 2, 2026
436dde4
test(bootstrap): forceExit so the suite terminates in CI
JOY Oct 2, 2026
3bedee5
fix(sync): split the self-hosted authorize endpoint out of OAuthContr…
JOY Oct 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,18 @@ BRAND_AFFILIATE_URL="https://example.com/affiliates"
# "Add to Claude" button β€” it defaults to empty on purpose so a deployment
# that forgets to set it cannot offer the upstream listing by accident.
BRAND_CLAUDE_DIRECTORY_URL=""
# Only needed if you run your own Postiz Canva app: its origin from the Canva
# Developer Portal (Security -> Credentials), allowed by CORS on the backend.
#CANVA_APP_ORIGIN="https://app-xxxxxxxxxxx.canva-apps.com"

# Sign in with Apple (login provider)
# APPLE_BUNDLE_ID: iOS app bundle id (native mobile sign-in)
# APPLE_SERVICE_ID / TEAM_ID / KEY_ID / PRIVATE_KEY: web sign-in (Services ID + .p8 key)
APPLE_BUNDLE_ID=""
APPLE_SERVICE_ID=""
APPLE_TEAM_ID=""
APPLE_KEY_ID=""
APPLE_PRIVATE_KEY=""

# ==============================================================================
# 8. Single Sign-On (Generic OAuth 2.0 / DOS ID via PKCE Bridge)
Expand Down Expand Up @@ -383,6 +395,30 @@ DRIBBBLE_CLIENT_SECRET="sample_dribbble_client_secret"
# --- Tumblr ---
TUMBLR_CLIENT_ID="sample_tumblr_client_id"
TUMBLR_CLIENT_SECRET="sample_tumblr_client_secret"
# Misc Settings
OPENAI_API_KEY=""
# OAuth client configured for the ChatGPT app. Only this client can receive
# verified user email claims for ChatGPT Enterprise domain restrictions.
OPENAI_OAUTH_CLIENT_ID=""
# MCP OAuth Dynamic Client Registration (RFC 7591) redirect-domain allowlist.
# When set (comma separated), POST /oauth/register only accepts redirect_uris
# whose host matches a listed domain or one of its subdomains; unset or empty
# means any client can self-register. Only https callbacks are checked:
# loopback (http://localhost:8787/callback - Grok, Claude Code) and
# private-use scheme callbacks (cursor://...) stay on the user's machine
# and are always accepted. Example locks web callbacks to the claude.ai
# connector; add cursor.com for Cursor.
# DCR_VERIFIED_DOMAINS="claude.ai,claude.com"
# Postiz Cloud only: lets people connect a self-hosted Postiz on the MCP
# consent screen ("Use self-hosted"). The MCP tool calls of that connection
# are relayed to the instance with its API key. Leave unset on self-hosted
# installs; set it on both the API and the MCP backend.
# MCP_SELF_HOSTED_RELAY=true
# EVOLINK_API_KEY="" # EvoLink API key for Seedance AI video generation (https://evolink.ai)
NEXT_PUBLIC_DISCORD_SUPPORT=""
NEXT_PUBLIC_POLOTNO=""
# NOT_SECURED=false
API_LIMIT=30 # The limit of the public API hour limit

# --- VK (VKontakte) ---
VK_ID="12345678"
Expand Down
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,10 @@ The backend app is mostly used to write controllers and import from the librarie
- Linting of the project can run only from the root.
- Use only pnpm.
- Branding guard (`scripts/branding-guard.ts`, enforced in CI) blocks reintroducing upstream endpoints or branding; use `branding-guard-allow:` comments only for deliberate references.
- Workflows files can never be changed if they are already in origin/main, because changing a workflow will fail all its activities, instead create a new workflow with the version, and everywhere the workflow being called, change it to the new workflow version.
- Workflows activities parameters cannot be changed, as it will break the workflow, if we need to change the parameters, if we need to change the parameters, we need to create a new activity with the new parameters, and then create a new workflow that uses the new activity.
- Code must always be generic, there can't be a way that a specific logic, let's say facebook or instagram, appear in a file that use a generic logic, instead, we need to edit the interface of the provider, add another function, and then generically call it from the generic code, and then implement the specific logic in the provider implementation. we can't have something like if(facebookProvider) {} inside a non facebook provider file.
- Before adding a field to a shared repository/service `select` or changing a shared method's return shape, grep for all its consumers (frontend, public API, MCP/agent tools, orchestrator, webhooks) and confirm the change is intended for each.
- Exposing a stored field to a new external surface (public API, MCP, webhooks) is a data-exposure decision β€” ask first, and check what existing production rows hold for that column before shipping it.
- Never silently change the meaning or format of a value persisted in an existing DB column; that affects every reader and all historical rows, so ask first.
- Never return a raw Prisma row from a model that holds credentials (`Integration`, `User` and similar) in a controller response. Repository writes on those models must use a `select` with only the fields the caller needs (like `setTimes` with `select: { id: true }`), or the controller returns nothing. This applies even where nearby code looks different.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@
| [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 |
| [ChatbotX](https://chatbotx.io/?ref=postiz) | <img src="https://github.com/user-attachments/assets/0aa6b058-9a64-46d3-bc26-337abc51737d" alt="ChatbotX" width="500"/> | The ManyChat alternative that you can self-host, white-label, and resell to your clients. Bring your own OpenClaw, Hermes, or Claude agents! |
| [RapidProxy](https://www.rapidproxy.io/?ref=postiz) | <img width="700" height="412" alt="WP-PROXY-THUMBNAIL-41" src="https://github.com/user-attachments/assets/6cf5b2c9-2687-4181-8964-2e7b90eace4f" /> | RapidProxy provides 90M+ residential IPs for social media, browser automation, and AI workflows, with smart rotation and stable sessions. From $0.55/GB; use RAPID10 for 10% off.

![Bronze Tier](https://opencollective.com/postiz/tiers/main-repository-bronze-tier.svg?avatarHeight=36&width=600&button=false)

# Intro

Expand Down
3 changes: 3 additions & 0 deletions apps/backend/src/api/api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
OAuthController,
OAuthAuthorizedController,
} from '@gitroom/backend/api/routes/oauth.controller';
import { OAuthSelfHostedController } from '@gitroom/backend/api/routes/oauth.selfhosted.controller';
import { AnnouncementsController } from '@gitroom/backend/api/routes/announcements.controller';
import { AdminController } from '@gitroom/backend/api/routes/admin.controller';
import { EcosystemModule } from '@gitroom/backend/ecosystem/ecosystem.module';
Expand Down Expand Up @@ -88,6 +89,7 @@ const authenticatedController = [
? [
RootController,
OAuthController,
OAuthSelfHostedController,
MediaWidgetController,
ClippingWidgetController,
]
Expand All @@ -101,6 +103,7 @@ const authenticatedController = [
EnterpriseController,
NoAuthIntegrationsController,
OAuthController,
OAuthSelfHostedController,
MediaWidgetController,
ClippingWidgetController,
...authenticatedController,
Expand Down
3 changes: 1 addition & 2 deletions apps/backend/src/api/routes/enterprise.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ export class EnterpriseController {
private verifyEnterpriseToken<T extends object>(params: string): T {
const payload = AuthService.verifyJWT(params) as any;
if (
!payload ||
typeof payload !== 'object' ||
!payload || typeof payload !== 'object' ||
'providerName' in payload || // login token (full User row)
'orgId' in payload || // team invite token
'expires' in payload // password reset token
Expand Down
10 changes: 10 additions & 0 deletions apps/backend/src/api/routes/integrations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { ApiTags } from '@nestjs/swagger';
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.time.dto';
import { CustomerNameDto } from '@gitroom/nestjs-libraries/dtos/integrations/customer.name.dto';
import { PlugDto } from '@gitroom/nestjs-libraries/dtos/plugs/plug.dto';
import {
Disconnect,
Expand Down Expand Up @@ -66,6 +67,15 @@ export class IntegrationsController {
return this._integrationService.customers(org.id);
}

@Put('/customers/:id')
async updateCustomerName(
@GetOrgFromRequest() org: Organization,
@Param('id') id: string,
@Body() body: CustomerNameDto
) {
return this._integrationService.updateCustomerName(org.id, id, body.name);
}

@Put('/:id/group')
async updateIntegrationGroup(
@GetOrgFromRequest() org: Organization,
Expand Down
16 changes: 16 additions & 0 deletions apps/backend/src/api/routes/oauth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { BootstrapService } from '@gitroom/backend/ecosystem/bootstrap.service';
import { AuthService as AuthChecker } from '@gitroom/helpers/auth/auth.service';
import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management';
import { ApiTags } from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
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';
Expand Down Expand Up @@ -53,6 +54,11 @@ export class OAuthController {
}
);

const selfHosted = this._oauthService.allowsSelfHosted(
app,
query.resource
);

return {
app: {
name: app.name,
Expand All @@ -62,10 +68,20 @@ export class OAuthController {
redirectUrl: app.redirectUrl,
},
state: query.state,
selfHosted,
selfHostedEmail:
selfHosted && this._oauthService.selfHostedRequiresEmail(app),
};
}

// Public (the person may have no account here) and capped per client,
// since every attempt sends requests to the instance. Moved to
// OAuthSelfHostedController (routes/oauth.selfhosted.controller.ts) so this
// file no longer pulls McpRelayService/@mastra into the consent test graph.

@Post('/token')
// RFC 6749 Β§5.1: successful token responses are 200; strict clients (Canva) reject Nest's default 201
@HttpCode(200)
async token(
@Body() body: TokenExchangeDto,
@Headers('authorization') authorization?: string
Expand Down
68 changes: 68 additions & 0 deletions apps/backend/src/api/routes/oauth.selfhosted.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
import { OAuthService } from '@gitroom/nestjs-libraries/database/prisma/oauth/oauth.service';
import { AuthorizeSelfHostedDto } from '@gitroom/nestjs-libraries/dtos/oauth/authorize-oauth.dto';
import { McpRelayService } from '@gitroom/nestjs-libraries/chat/mcp.relay.service';
import { ThrottlerRealIpGuard } from '@gitroom/nestjs-libraries/throttler/throttler.provider';

// Split out of OAuthController so the bootstrap/consent test suite (which
// imports OAuthAuthorizedController) does not pull McpRelayService - and
// through it @mastra/core and ESM-only dependencies - into its module graph.
@ApiTags('OAuth')
@Controller('/oauth')
export class OAuthSelfHostedController {
constructor(
private _oauthService: OAuthService,
private _mcpRelayService: McpRelayService
) {}

// Public (the person may have no account here) and capped per client,
// since every attempt sends requests to the instance
@UseGuards(ThrottlerRealIpGuard)
@Throttle({ default: { limit: 30, ttl: 3600000 } })
@Post('/authorize/self-hosted')
async authorizeSelfHosted(@Body() body: AuthorizeSelfHostedDto) {
const app = await this._oauthService.validateAuthorizationRequest(
body.client_id,
{
redirectUri: body.redirect_uri,
codeChallenge: body.code_challenge,
codeChallengeMethod: body.code_challenge_method,
}
);

const email = body.email?.trim();
this._oauthService.validateSelfHostedRequest(app, {
resource: body.resource,
email,
});

const instance = await this._mcpRelayService.connect(
body.instance_url,
body.api_key
);

const code = await this._oauthService.createSelfHostedAuthorizationCode(
app.id,
{ ...instance, email },
app.dynamic
? {
codeChallenge: body.code_challenge,
codeChallengeMethod: body.code_challenge_method,
redirectUri: body.redirect_uri,
}
: undefined
);

// Same redirect as an approved cloud authorization
const redirectUrl = new URL(
app.dynamic ? body.redirect_uri! : app.redirectUrl
);
redirectUrl.searchParams.set('code', code);
if (body.state) {
redirectUrl.searchParams.set('state', body.state);
}
return { redirect: redirectUrl.toString() };
}
}
9 changes: 9 additions & 0 deletions apps/backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ async function start() {
process.env.FRONTEND_URL,
'http://localhost:6274',
...(process.env.MAIN_URL ? [process.env.MAIN_URL] : []),
// Optional: the Canva app calls the public API from its iframe origin
// (browsers send it lowercase, e.g. https://app-aabbcc.canva-apps.com)
...(process.env.CANVA_APP_ORIGIN
? [
process.env.CANVA_APP_ORIGIN.trim()
.replace(/\/+$/, '')
.toLowerCase(),
]
: []),
],
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { streamUploadOptions } from '@gitroom/nestjs-libraries/upload/multer.str
import { ApiTags } from '@nestjs/swagger';
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
import { GetIncludeDeletedFromRequest } from '@gitroom/nestjs-libraries/user/include.deleted.from.request';
import { GetOAuthUserIdFromRequest } from '@gitroom/nestjs-libraries/user/oauth.user.id.from.request';
import { Organization } from '@prisma/client';
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
Expand Down Expand Up @@ -229,6 +230,24 @@ export class PublicIntegrationsController {
return { connected: true };
}

// `user` is only known for OAuth app tokens; an API key belongs to the
// whole organization
@Get('/me')
async getMe(
@GetOrgFromRequest() org: Organization,
@GetOAuthUserIdFromRequest() userId?: string
) {
Sentry.metrics.count('public_api-request', 1);
const user = userId ? await this._usersService.getPersonal(userId) : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The _usersService is used here to fetch the personal user details, but UsersService is not imported or injected in the constructor of PublicIntegrationsController. This will cause a runtime TypeError when calling the /me endpoint.


return {
organization: { id: org.id, name: org.name },
user: user
? { id: user.id, name: user.name, picture: user.picture?.path || null }
: null,
};
}

@Get('/groups')
async listGroups(@GetOrgFromRequest() org: Organization) {
Sentry.metrics.count('public_api-request', 1);
Expand Down
3 changes: 3 additions & 0 deletions apps/backend/src/services/auth/public.auth.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export class PublicAuthMiddleware implements NestMiddleware {
}

org = authorization.organization;
// The user who approved the OAuth app, so /me can show who is connected
// @ts-ignore
req.oauthUserId = authorization.userId;
} else {
org = await this._organizationService.getOrgByApiKey(auth);
if (!org) {
Expand Down
9 changes: 9 additions & 0 deletions apps/frontend/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- BEGIN:nextjs-agent-rules -->

# This is NOT the Next.js you know

This version has breaking changes β€” APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.

This block is written and re-added by `next dev` β€” verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.

<!-- END:nextjs-agent-rules -->
1 change: 1 addition & 0 deletions apps/frontend/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
27 changes: 25 additions & 2 deletions apps/frontend/src/app/(app)/api/uploads/[[...path]]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,43 @@ export const GET = async (
if (filePath !== base && !filePath.startsWith(base + sep)) {
return new NextResponse('Not found', { status: 404 });
}
const response = createReadStream(filePath);
const fileStats = statSync(filePath);
const contentType = mime.getType(filePath) || 'application/octet-stream';

// Honor ranged requests: providers that push video in chunks (TikTok,
// YouTube, LinkedIn, X) fetch byte windows with a Range header and reject
// anything but a 206, so ignoring Range breaks their uploads.
const range = /^bytes=(\d+)-(\d*)$/.exec(request.headers.get('range') || '');
const start = range ? Number(range[1]) : 0;
const end =
range && range[2]
? Math.min(Number(range[2]), fileStats.size - 1)
: fileStats.size - 1;

if (range && (start >= fileStats.size || start > end)) {
return new NextResponse(null, {
status: 416,
headers: { 'Content-Range': `bytes */${fileStats.size}` },
});
}

const response = createReadStream(filePath, range ? { start, end } : {});
const iterator = nodeStreamToIterator(response);
const webStream = iteratorToStream(iterator);
return new Response(webStream, {
status: range ? 206 : 200,
headers: {
'Content-Type': contentType,
// Set the appropriate content-type header
'Content-Length': fileStats.size.toString(),
'Content-Length': (end - start + 1).toString(),
// Set the content-length header
'Last-Modified': fileStats.mtime.toUTCString(),
// Set the last-modified header
'Cache-Control': 'public, max-age=31536000, immutable', // Example cache-control header
'Accept-Ranges': 'bytes',
...(range
? { 'Content-Range': `bytes ${start}-${end}/${fileStats.size}` }
: {}),
},
});
};
22 changes: 4 additions & 18 deletions apps/frontend/src/app/global-error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,17 @@
import * as Sentry from '@sentry/nextjs';
import NextError from 'next/error';
import { useEffect } from 'react';
import { useVariables } from '@gitroom/react/helpers/variable.context';

export default function GlobalError({
error,
}: {
error: Error & { digest?: string };
}) {
const { sentryDsn } = useVariables();

useEffect(() => {
if (!sentryDsn) {
return;
}
const eventId = Sentry.captureException(error);
Sentry.showReportDialog({
eventId,
title: 'Something broke!',
subtitle: 'Please help us fix the issue by providing some details.',
labelComments: 'What happened?',
labelName: 'Your name',
labelEmail: 'Your email',
labelSubmit: 'Send Report',
lang: 'en',
});

// The variables context is not mounted here (this replaces the root
// layout), so don't gate on its DSN. Without a client this is a no-op, and
// beforeSend already opens the report dialog for captured exceptions
Sentry.captureException(error);
}, [error]);
return (
<html>
Expand Down
Loading
Loading