Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
143 commits
Select commit Hold shift + click to select a range
751ce66
feat(tags): expose tag editing in the tags dropdown
giladresisi Aug 3, 2026
d9bae16
fix(tags): readable calendar tag labels on any tag color
giladresisi Aug 3, 2026
1f7e1b6
fix(tags): always restore dropdown closability after edit/delete modals
giladresisi Aug 4, 2026
49aa92a
fix(integrations): keep the new refresh token when reconnecting a cha…
giladresisi Aug 12, 2026
5cd4de2
feat: list media via public API and MCP mediaListTool
giladresisi Aug 19, 2026
063ee50
fix: validate page query on GET /public/v1/media
giladresisi Aug 19, 2026
28e7167
fix: guard against duplicate Stripe subscriptions per customer
giladresisi Sep 10, 2026
c3f39b9
fix: use console.log for the new stripe guard log lines
giladresisi Sep 10, 2026
76d6616
fix: reject in-app cancel when the org has no live Stripe subscription
giladresisi Sep 10, 2026
5fb2f53
chore: drop unused Injectable import from stripe.service
giladresisi Sep 10, 2026
328b34c
fix(billing): persist cancelAt on cancel/reactivate instead of waitin…
giladresisi Sep 21, 2026
8edbc60
Merge branch 'main' into fix/duplicate-subscriptions
giladresisi Sep 21, 2026
291b07b
fix(calendar): surface curated provider errors in the calendar tooltip
giladresisi Jul 28, 2026
e333f75
fix(calendar): clear the stored error when a post fails again uncurat…
giladresisi Aug 11, 2026
c8bf9d8
fix(calendar): curate post errors at read time instead of write time
giladresisi Aug 28, 2026
8202a15
feat(mcp): expose the curated post error in the posts list tool
giladresisi Sep 22, 2026
4a6ba07
feat(subscription): re-enable disabled channels when the new plan can…
giladresisi Aug 28, 2026
c145f0c
feat(subscription): only re-enable channels when the limit grows, tel…
giladresisi Sep 28, 2026
70911f8
chore(i18n): translate the post-checkout channel notices
giladresisi Sep 28, 2026
5848ef2
refactor(integrations): enable all channels with a single updateMany
giladresisi Sep 28, 2026
0cf4596
fix(frontend): skip the Sentry report dialog for errors from browser …
giladresisi Sep 28, 2026
277d362
fix(frontend): record the Sentry wrapper location synchronously at init
giladresisi Sep 28, 2026
5a1e92b
feat(channels): rename a channel in Postiz while keeping its original…
giladresisi Sep 30, 2026
fa47229
fix(frontend): classify third-party errors only in the browser
giladresisi Sep 30, 2026
9aa7c88
Merge remote-tracking branch 'origin/main' into feat/rename-channel
giladresisi Oct 1, 2026
bb2e017
fix(channels): return only the id from the custom name update
giladresisi Oct 1, 2026
e70110f
feat(calendar): add an open-post link icon for published posts
giladresisi Aug 17, 2026
163cfbf
feat(calendar): show the open-post link on failed posts that still ha…
giladresisi Sep 10, 2026
9af1eb8
fix(calendar): open the first URL of a multi-target post and sever th…
giladresisi Sep 10, 2026
f10296a
feat(calendar): resolve the release URL on click before opening the post
giladresisi Oct 2, 2026
fc5b4ca
fix(calendar): open the post tab before the release URL request
giladresisi Oct 2, 2026
4583ac5
feat: fix some tools
nevo-david Oct 2, 2026
6b102af
feat: destructiveHint
nevo-david Oct 2, 2026
f66844f
docs(security): a role address for time-critical reports instead of a…
egelhaus Oct 2, 2026
e25dd93
feat(security): clarify new email entry
egelhaus Oct 2, 2026
3964536
Merge pull request #2195 from gitroomhq/security-contact-role-address
egelhaus Oct 2, 2026
2aae02f
feat: inline picture
nevo-david Oct 3, 2026
28701a9
feat: mcp and skill description
nevo-david Oct 3, 2026
ce9cb27
Merge remote-tracking branch 'origin/main'
nevo-david Oct 3, 2026
a4bebec
fix(settings): hide the Teams tab from the User role
giladresisi Oct 3, 2026
185044d
feat: fix workflows without continue as new
nevo-david Oct 4, 2026
1f5f24e
feat: workflow changes
nevo-david Oct 4, 2026
f65de34
feat: redeploy backend and mcp
nevo-david Oct 4, 2026
36834cd
fix: typo in password reset email
nevo-david Oct 4, 2026
273c7b5
feat: streak load
nevo-david Oct 4, 2026
c9138d3
feat: fix agent media sso
nevo-david Oct 4, 2026
f28c252
feat: make ugc
nevo-david Oct 4, 2026
d883bfa
feat: fix auto post
nevo-david Oct 4, 2026
96b7847
fixes: analytics
nevo-david Oct 4, 2026
4e55a95
feat: provider increase
nevo-david Oct 4, 2026
ac27b00
feat(sponsors): remove rapidproxy from readme
egelhaus Oct 4, 2026
8c5f356
fix(settings): show a warning toast when the invite request fails
giladresisi Oct 5, 2026
94a8891
Merge pull request #2200 from gitroomhq/fix/teams-tab-user-role
giladresisi Oct 5, 2026
8630c48
Merge pull request #1828 from gitroomhq/feat/tag-edit-rewire
giladresisi Oct 5, 2026
fc7f764
Merge pull request #1829 from gitroomhq/fix/tag-label-contrast
giladresisi Oct 5, 2026
9e6b99c
Merge pull request #2194 from gitroomhq/feat/calendar-open-post-link-…
giladresisi Oct 5, 2026
78182ed
Merge pull request #2119 from gitroomhq/fix/calendar-mapped-error-too…
giladresisi Oct 5, 2026
e55121d
Merge pull request #2112 from gitroomhq/fix/cancel-toggle
nevo-david Oct 5, 2026
b3b074a
Merge pull request #2165 from gitroomhq/fix/sentry-third-party-tag
nevo-david Oct 5, 2026
1af2885
Merge pull request #2190 from gitroomhq/feat/rename-channel
nevo-david Oct 5, 2026
8809e45
Merge remote-tracking branch 'origin/main' into fix/duplicate-subscri…
giladresisi Oct 5, 2026
256f2b4
docs(claude): code invoked by external webhooks must not throw
giladresisi Oct 5, 2026
2917384
Merge remote-tracking branch 'origin/main' into fix/enable-channels-o…
giladresisi Oct 5, 2026
5d8ce5a
Merge pull request #2202 from gitroomhq/docs/webhook-no-throw-rule
giladresisi Oct 5, 2026
2b4fcbe
fix(subscription): log webhook-path failures instead of swallowing them
giladresisi Oct 5, 2026
8ad0df3
fix(api): return only the needed fields from channel delete and the o…
giladresisi Oct 1, 2026
4913031
fix: skip cancelling an already-cancelled duplicate on webhook retry
giladresisi Oct 5, 2026
78558e9
Merge pull request #2160 from gitroomhq/fix/enable-channels-on-upgrad…
giladresisi Oct 5, 2026
ac65e20
fix: never treat incomplete Stripe subscriptions as live
giladresisi Oct 5, 2026
86b3c3d
Merge pull request #2061 from gitroomhq/fix/duplicate-subscriptions
giladresisi Oct 5, 2026
b95168f
feat: show login method in Global Settings
giladresisi Oct 5, 2026
b08bad8
feat(calendar): frame published posts in green with a checkmark
giladresisi Oct 5, 2026
169c66a
fix(calendar): make the post delete icon and confirmation red, swap D…
giladresisi Oct 5, 2026
743b996
fix: show Apple login in Authentication settings
giladresisi Oct 6, 2026
9bf96eb
feat(calendar): add channel multi-select filter above the calendar
giladresisi Oct 6, 2026
f0113c7
chore(i18n): translations for the channel filter keys
giladresisi Oct 6, 2026
2a2c85c
fix(calendar): filter list view by channel on the server
giladresisi Oct 6, 2026
4990043
fix(posts): find-slot never responds when all channels are disabled
giladresisi Aug 28, 2026
1e57cd3
Merge pull request #2207 from gitroomhq/fix/find-slot-all-disabled-main
giladresisi Oct 6, 2026
5007164
Merge pull request #2203 from gitroomhq/feat/settings-authentication
giladresisi Oct 6, 2026
f05a049
Merge pull request #2204 from gitroomhq/fix/calendar-delete-red
giladresisi Oct 6, 2026
86ba0f4
Merge pull request #2205 from gitroomhq/feat/calendar-channel-filter
giladresisi Oct 6, 2026
fffe4fa
fix(calendar): anchor the post state badges with start instead of lef…
giladresisi Oct 6, 2026
5cfdccc
Merge pull request #2206 from gitroomhq/feat/calendar-published-post-…
giladresisi Oct 6, 2026
1d4b75f
feat: auto text direction (RTL) in composer editor and previews
giladresisi Aug 30, 2026
75cb2f8
fix: allow dir attribute through post content sanitizer
giladresisi Aug 30, 2026
53ea9c8
fix: handle p tags with attributes and isolate IG caption direction
giladresisi Aug 30, 2026
ec01d33
fix: match paragraph tag shape when detecting HTML content
giladresisi Aug 30, 2026
0807837
fix: markdown conversion regexes tolerate paragraph and heading attri…
giladresisi Aug 30, 2026
326719b
fix(permissions): look up the refreshed channel by internalId in the …
giladresisi Oct 6, 2026
109e8d4
Merge pull request #2208 from gitroomhq/fix/refresh-channel-limit
giladresisi Oct 6, 2026
48aa7e2
feat: new creation modal
nevo-david Oct 6, 2026
c3743d9
feat: creation modal
nevo-david Oct 6, 2026
afa30c6
Merge pull request #2209 from gitroomhq/rtl-auto-direction-main
giladresisi Oct 6, 2026
6f79545
fix(calendar): open-post icon no longer lands on a wrong or missing T…
giladresisi Oct 6, 2026
506d9a0
chore(i18n): translations for the open-post link toasts
giladresisi Oct 6, 2026
bf89e2e
feat(sponsors): re-add chatbotx to readme
egelhaus Oct 6, 2026
22c0341
Merge pull request #2210 from gitroomhq/egelhaus-patch-1
egelhaus Oct 6, 2026
6eed47e
feat(tiktok): connect UPLOAD drafts to the published post from the op…
giladresisi Oct 6, 2026
541c7a4
fix(tiktok-business): store the post URL when a post is connected by …
giladresisi Oct 6, 2026
66d2101
fix(instagram): map the Trial Reel publish limit error to a readable …
giladresisi Oct 7, 2026
05c9690
Merge remote-tracking branch 'origin/main' into feat/media-list
giladresisi Oct 7, 2026
c45ce4a
Merge pull request #2215 from gitroomhq/fix/instagram-trial-reel-limi…
giladresisi Oct 7, 2026
0992c46
feat: point MCP upload tools to the CLI and public API for private files
giladresisi Oct 7, 2026
1396b21
Merge pull request #2216 from gitroomhq/feat/upload-tool-hints
giladresisi Oct 7, 2026
e7b8126
fix: drop type and fileSize from the media list response
giladresisi Oct 7, 2026
4be38e1
fix: return only media-owned fields from the media list API and tool
giladresisi Oct 7, 2026
dda966e
fix: validate page input on mediaListTool
giladresisi Oct 7, 2026
fd0480f
Merge branch 'main' into feat/media-list
giladresisi Oct 7, 2026
07745d1
Merge pull request #1926 from gitroomhq/feat/media-list
giladresisi Oct 7, 2026
80d527b
fix(frontend): honor the AM/PM date metric in the post date picker ti…
giladresisi Sep 7, 2026
24b77c2
Merge pull request #2217 from gitroomhq/fix/date-picker-time-format-main
giladresisi Oct 7, 2026
00ff699
Merge pull request #2211 from gitroomhq/fix/open-post-pending-and-pho…
giladresisi Oct 7, 2026
3128e87
Merge pull request #2212 from gitroomhq/feat/tiktok-upload-draft-resolve
giladresisi Oct 7, 2026
a194e3f
feat: group schedules
nevo-david Oct 7, 2026
e756fa5
fix: responsive
nevo-david Oct 7, 2026
47175b5
feat: calendar line
nevo-david Oct 8, 2026
a9cdda5
Merge remote-tracking branch 'origin/fix/scope-sensitive-responses'
nevo-david Oct 8, 2026
3f848cc
fix: remove data loss
nevo-david Oct 8, 2026
003a77e
feat: fix settings
nevo-david Oct 8, 2026
adf1a8f
fix(calendar): put Delete right-most in the post hover bar, after Pre…
giladresisi Oct 8, 2026
2361947
Merge pull request #2221 from gitroomhq/fix/calendar-icon-order
giladresisi Oct 8, 2026
7b29d40
feat: move menu items
nevo-david Oct 8, 2026
0969ce7
Merge remote-tracking branch 'origin/main'
nevo-david Oct 8, 2026
5ac6e10
feat: fix icon
nevo-david Oct 8, 2026
3bd88ce
feat: professional billing
nevo-david Oct 8, 2026
8f3c38a
feat: fix invoices
nevo-david Oct 8, 2026
4c5d42a
feat: superadmin public api endpoint to reset an org's ai credits
egelhaus Oct 8, 2026
2e2d8ed
fix(integrations): log the error behind "Authentication failed" on so…
giladresisi Oct 9, 2026
bc61335
Merge pull request #2225 from gitroomhq/fix/log-social-connect-auth-f…
giladresisi Oct 9, 2026
0d486f7
Merge origin/main into fix/google-workspace-session-observability
giladresisi Oct 9, 2026
d6f881a
fix(account): ignore disabled members when checking account deletion
giladresisi Oct 9, 2026
6c50457
Merge pull request #2226 from gitroomhq/fix/delete-account-ignore-dis…
giladresisi Oct 9, 2026
7db798f
Merge branch 'main' into fix/google-workspace-session-observability
giladresisi Oct 9, 2026
91c91f6
Merge pull request #1884 from gitroomhq/fix/google-workspace-session-…
giladresisi Oct 9, 2026
d7eb12a
merge: upstream main / v2.25.1 (8e42f09c..91c91f63, 135 commits)
claude Oct 11, 2026
23a37cd
fix(sync): brand the v2.25.1 upload tool hints and YouTube Workspace …
claude Oct 11, 2026
a445c39
fix(sync): clear the React Compiler lint errors in the v2.25.1 lines
claude Oct 11, 2026
a0be876
docs(fork-delta): full paths and accurate wording for the v2.25.1 lin…
claude Oct 11, 2026
813f4d5
fix(sync): keep the connect-failure log format string constant
claude Oct 11, 2026
540a972
fix(sync): clear the ESLint warnings on the v2.25.1 lines
claude Oct 11, 2026
6114dc5
fix(sync): decode & last in the html branch of stripHtmlValidation
claude Oct 11, 2026
aa4172c
Merge pull request #69 from DOS/claude/elegant-babbage-eqmnvp
JOY Oct 12, 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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,4 @@ The backend app is mostly used to write controllers and import from the librarie
- 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.
- Be extra careful with code invoked by webhooks of external systems (Stripe, RevenueCat and other payment providers), including everything they call down the stack (e.g. `SubscriptionService.createOrUpdateSubscription`, `modifySubscription`, `deleteSubscription`). That code must not throw exceptions: never add a `throw`, and wrap any call that can fail (DB, provider API) so a failure is caught and logged instead of propagating. A webhook that throws leaves billing state half-applied for a paying customer.
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ If you discover a security vulnerability in the Postiz app, report it through [G

Email the maintainers only when the report requires immediate, time-critical attention. Email does not replace submitting the report through GAdvisory.

- @egelhaus ([E-Mail](mailto:egelhaus@ennogelhaus.de))
- Gelhaus Solutions (@egelhaus): [contact@gplatform.org](mailto:contact@gplatform.org)

### AI Reports

Expand Down
37 changes: 29 additions & 8 deletions apps/backend/src/api/routes/billing.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/us
import { DosSharedBillingService } from '@gitroom/nestjs-libraries/dos-billing/dos-shared-billing.service';
import { PaymentService } from '@gitroom/nestjs-libraries/services/payment/payment.service';
import { BillingSyncDto } from '@gitroom/nestjs-libraries/dtos/billing/billing.sync.dto';
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
import {
AuthorizationActions,
Sections,
} from '@gitroom/backend/services/auth/permissions/permission.exception.class';

@ApiTags('Billing')
@Controller('/billing')
Expand Down Expand Up @@ -178,6 +183,12 @@ export class BillingController {
};
}

@Get('/invoices')
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
async getInvoices(@GetOrgFromRequest() org: Organization) {
return (await this.provider(org)).getInvoices(org.id);
}

@Get('/')
async getCurrentBilling(
@GetOrgFromRequest() org: Organization,
Expand All @@ -198,15 +209,14 @@ export class BillingController {
@GetUserFromRequest() user: User,
@Body() body: { feedback: string }
) {
await this._notificationService.sendEmail(
process.env.EMAIL_FROM_ADDRESS,
'Subscription Cancelled',
`Organization ${org.name} has cancelled their subscription because: ${body.feedback}`,
user.email
);

if (this._dosBilling.enabled()) {
await this._dosBilling.cancel(user);
await this._notificationService.sendEmail(
process.env.EMAIL_FROM_ADDRESS,
'Subscription Cancelled',
`Organization ${org.name} has cancelled their subscription because: ${body.feedback}`,
user.email
);
try {
await this._dosBilling.syncOrg(user, org.id);
} catch {}
Expand All @@ -215,7 +225,18 @@ export class BillingController {
return { cancel_at: sub?.cancelAt || null };
}

return (await this.provider(org)).setToCancel(org.id);
const result = await (await this.provider(org)).setToCancel(org.id);

if (result.cancel_at) {

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 setToCancel method in StripeService returns an object with cancelAt (camelCase), but here the code checks result.cancel_at (snake_case). This mismatch causes the condition to always evaluate to false, preventing the cancellation email notification from being sent.

Suggested change
if (result.cancel_at) {
if (result.cancelAt) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Leaving this as is. StripeService.setToCancel returns { id, cancel_at } in snake_case on all three paths: un-cancel, immediate cancel after a failed payment, and cancel at period end (stripe.service.ts). So result.cancel_at is the right key and the email is sent whenever a cancel is set. Switching to cancelAt would stop the email.


Generated by Claude Code

await this._notificationService.sendEmail(
process.env.EMAIL_FROM_ADDRESS,
'Subscription Cancelled',
`Organization ${org.name} has cancelled their subscription because: ${body.feedback}`,
user.email
);
}

return result;
}

@Post('/prorate')
Expand Down
13 changes: 12 additions & 1 deletion 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 { IntegrationNameDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.name.dto';
import { CustomerNameDto } from '@gitroom/nestjs-libraries/dtos/integrations/customer.name.dto';
import { PlugDto } from '@gitroom/nestjs-libraries/dtos/plugs/plug.dto';
import {
Expand Down Expand Up @@ -98,6 +99,15 @@ export class IntegrationsController {
return this._integrationService.updateOnCustomerName(org.id, id, body.name);
}

@Put('/:id/custom-name')
async updateCustomName(
@GetOrgFromRequest() org: Organization,
@Param('id') id: string,
@Body() body: IntegrationNameDto
) {
return this._integrationService.updateCustomName(org.id, id, body.name);
}

@Get('/list')
async getIntegrationList(@GetOrgFromRequest() org: Organization) {
return {
Expand All @@ -109,7 +119,8 @@ export class IntegrationsController {
p.providerIdentifier
);
return {
name: p.name,
name: p.customName || p.name,
originalName: p.name,
id: p.id,
internalId: p.internalId,
disabled: p.disabled,
Expand Down
16 changes: 14 additions & 2 deletions apps/backend/src/api/routes/no.auth.integrations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ export class NoAuthIntegrationsController {
? 'none'
: await ioRedis.get(`login:${body.state}`);
if (!getCodeVerifier) {
throw new Error('Invalid state');
// The state expires after an hour and is deleted once used (page reload)
throw new HttpException(
'This connection link expired or was already used, please connect the channel again',
400
);
}

const organization = await ioRedis.get(`organization:${body.state}`);
Expand Down Expand Up @@ -137,7 +141,14 @@ export class NoAuthIntegrationsController {
refresh,
auth.accessToken
);
return res({ ...newAuth, refreshToken: body.refresh });
// keep the fresh refresh token and expiration from the new
// authentication, otherwise the stored refresh token is wiped and
// the channel breaks again as soon as the access token expires
return res({
...newAuth,
refreshToken: auth.refreshToken,
expiresIn: auth.expiresIn,
});
} catch (err: any) {
return res({
error: err.message,
Expand Down Expand Up @@ -165,6 +176,7 @@ export class NoAuthIntegrationsController {
});
}

console.log(`Authentication failed for ${integration}:`, err);
return res({
error: 'Authentication failed',
accessToken: '',
Expand Down
8 changes: 8 additions & 0 deletions apps/backend/src/api/routes/posts.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ export class PostsController {
return this._postsService.getMissingContent(org.id, id);
}

@Get('/:id/release-url')
async getReleaseURL(
@GetOrgFromRequest() org: Organization,
@Param('id') id: string
) {
return this._postsService.getReleaseURL(org.id, id);
}

@Put('/:id/release-id')
async updateReleaseId(
@GetOrgFromRequest() org: Organization,
Expand Down
4 changes: 3 additions & 1 deletion apps/backend/src/api/routes/public.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,10 @@ export class PublicController {

@Get(`/posts/:id`)
async getPreview(@Param('id') id: string) {
// the batch only links the posts in the dashboard, it stays out of the
// public preview
return (await this._postsService.getPostsRecursively(id, true)).map(
({ childrenPost, ...p }) => ({
({ childrenPost, batchId, ...p }) => ({
...p,
...(p.integration
? {
Expand Down
13 changes: 9 additions & 4 deletions apps/backend/src/api/routes/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export class UsersController {
process.env.AGENT_MEDIA_SSO_KEY
);

return { url: `https://agent-media.ai/sso/${token}` };
return { url: `https://app.agent-media.ai/sso/${token}` };
}

@Get('/self')
Expand Down Expand Up @@ -319,9 +319,14 @@ export class UsersController {

@Get('/organizations')
async getOrgs(@GetUserFromRequest() user: User) {
return (await this._orgService.getOrgsByUserId(user.id)).filter(
(f) => !f.users[0].disabled
);
return (await this._orgService.getOrgsByUserId(user.id))
.filter((f) => !f.users[0].disabled)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Accessing f.users[0].disabled directly is unsafe if the users array is empty, which will throw a TypeError. Use optional chaining to safely access the property.

Suggested change
.filter((f) => !f.users[0].disabled)
.filter((f) => !f.users?.[0]?.disabled)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Leaving this as is, and it was already answered on #69. getOrgsByUserId only returns orgs where users: { some: { userId } } matches, and its include filters users to that same userId. So every returned org has that row at users[0]. This filter also predates the sync.


Generated by Claude Code

.map((p) => ({
id: p.id,
name: p.name,
users: p.users,
subscription: p.subscription,
}));
}

@Post('/organizations')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
UseInterceptors,
} from '@nestjs/common';
import { streamUploadOptions } from '@gitroom/nestjs-libraries/upload/multer.stream.engine';
import { omit } from 'lodash';
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';
Expand All @@ -34,6 +35,7 @@ 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 { GetMediaDto } from '@gitroom/nestjs-libraries/dtos/media/get.media.dto';
import { ClippingDto } from '@gitroom/nestjs-libraries/dtos/clipping/clipping.dto';
import { ClippingService } from '@gitroom/nestjs-libraries/database/prisma/clipping/clipping.service';
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
Expand All @@ -54,6 +56,8 @@ import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
import { AdminStatsService } from '@gitroom/nestjs-libraries/database/prisma/admin-stats/admin-stats.service';
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
import { GetOrgActivityDto } from '@gitroom/nestjs-libraries/dtos/analytics/get.org.activity.dto';
import { ResetCreditsDto } from '@gitroom/nestjs-libraries/dtos/billing/reset.credits.dto';
import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
import dayjs from 'dayjs';

@ApiTags('Public API')
Expand All @@ -69,7 +73,8 @@ export class PublicIntegrationsController {
private _usersService: UsersService,
private _adminStatsService: AdminStatsService,
private _organizationService: OrganizationService,
private _clippingService: ClippingService
private _clippingService: ClippingService,
private _subscriptionService: SubscriptionService
) {}

@Post('/upload')
Expand Down Expand Up @@ -133,7 +138,14 @@ export class PublicIntegrationsController {
) {
Sentry.metrics.count('public_api-request', 1);
const body = await this._postsService.mapTypeToPost(
rawBody,
{
...rawBody,
// a date per channel is a dashboard feature, the public API keeps one
// date for the whole request
posts: Array.isArray(rawBody?.posts)
? rawBody.posts.map((post: any) => omit(post, 'date'))
: rawBody?.posts,
},
org.id,
rawBody?.type === 'draft' || true
);
Expand Down Expand Up @@ -230,6 +242,30 @@ export class PublicIntegrationsController {
return { connected: true };
}

@Get('/media')
async getMedia(
@GetOrgFromRequest() org: Organization,
@Query() query: GetMediaDto
) {
Sentry.metrics.count('public_api-request', 1);
const { pages, results } = await this._mediaService.getMedia(
org.id,
query.page,
query.search
);

return {
pages,
results: results.map((p) => ({
id: p.id,
name: p.name,
originalName: p.originalName,
path: p.path,
createdAt: p.createdAt,
})),
};
}

// `user` is only known for OAuth app tokens; an API key belongs to the
// whole organization
@Get('/me')
Expand Down Expand Up @@ -269,7 +305,7 @@ export class PublicIntegrationsController {
.filter((integration) => !group || integration.customer?.id === group)
.map((integration) => ({
id: integration.id,
name: integration.name,
name: integration.customName || integration.name,
identifier: integration.providerIdentifier,
picture: integration.picture,
disabled: integration.disabled,
Expand Down Expand Up @@ -411,6 +447,25 @@ export class PublicIntegrationsController {
});
}

@Post('/credits/reset')
@UseGuards(SuperAdminGuard)
async resetCredits(
@GetOrgFromRequest() org: Organization,
@Body() body: ResetCreditsDto
) {
Sentry.metrics.count('public_api-request', 1);
const reset = await this._subscriptionService.resetCredits(
org.id,
body.type
);

if (!reset) {
throw new HttpException({ msg: 'No subscription found' }, 404);
}

return reset;
}

@Get('/notifications')
async getNotifications(
@GetOrgFromRequest() org: Organization,
Expand Down Expand Up @@ -573,7 +628,13 @@ export class PublicIntegrationsController {
@Body('releaseId') releaseId: string
) {
Sentry.metrics.count('public_api-request', 1);
return this._postsService.updateReleaseId(org.id, id, releaseId);
// the batch only links the posts in the dashboard
const { batchId, ...post } = await this._postsService.updateReleaseId(
org.id,
id,
releaseId
);
return post;
}

@Get('/analytics/:integration')
Expand Down
16 changes: 6 additions & 10 deletions apps/backend/src/services/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/o
import { AuthService as AuthChecker } from '@gitroom/helpers/auth/auth.service';
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';
import { EmailService } from '@gitroom/nestjs-libraries/services/email.service';
import { NewsletterService } from '@gitroom/nestjs-libraries/newsletter/newsletter.service';
Expand All @@ -18,7 +17,6 @@ export class AuthService {
constructor(
private _userService: UsersService,
private _organizationService: OrganizationService,
private _notificationService: NotificationService,
private _emailService: EmailService,
private _providerManager: AuthProviderManager
) {}
Expand Down Expand Up @@ -74,11 +72,10 @@ export class AuthService {
: false;

const obj = { addedOrg, jwt: await this.jwt(create.users[0].user) };
await this._emailService.sendEmail(
await this._emailService.sendEmailSync(
body.email,
'Activate your account',
`Click <a href="${process.env.FRONTEND_URL}/auth/activate/${obj.jwt}">here</a> to activate your account`,
'top'
`Click <a href="${process.env.FRONTEND_URL}/auth/activate/${obj.jwt}">here</a> to activate your account`
);
return obj;
}
Expand Down Expand Up @@ -301,10 +298,10 @@ export class AuthService {
expires: dayjs().add(20, 'minutes').format('YYYY-MM-DD HH:mm:ss'),
});

await this._notificationService.sendEmail(
await this._emailService.sendEmailSync(
user.email,
'Reset your password',
`You have requested to reset your passsord. <br />Click <a href="${process.env.FRONTEND_URL}/auth/forgot/${resetValues}">here</a> to reset your password<br />The link will expire in 20 minutes`
`You have requested to reset your password. <br />Click <a href="${process.env.FRONTEND_URL}/auth/forgot/${resetValues}">here</a> to reset your password<br />The link will expire in 20 minutes`
);
}

Expand Down Expand Up @@ -354,11 +351,10 @@ export class AuthService {

const jwt = await this.jwt(user);

await this._emailService.sendEmail(
await this._emailService.sendEmailSync(
user.email,
'Activate your account',
`Click <a href="${process.env.FRONTEND_URL}/auth/activate/${jwt}">here</a> to activate your account`,
'top'
`Click <a href="${process.env.FRONTEND_URL}/auth/activate/${jwt}">here</a> to activate your account`
);

return true;
Expand Down
Loading
Loading