Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 19 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,25 @@ MEWE_HOST="https://mewe.com"
MEWE_APP_ID="sample_mewe_app_id"
MEWE_API_KEY="sample_mewe_api_key"

# DOS shared billing — Crove sells DOS Plus ($9) / Pro ($19), not Postiz Stripe packages.
# Keep STRIPE_PUBLISHABLE_KEY set so channel gating stays on. Checkout goes to DOS-Me.
# DOS_SHARED_BILLING=true
# DOS_ME_API_URL=https://api.dos.me
# DOS_ME_INTERNAL_API_KEY=""

# Developer Settings
NX_ADD_PLUGINS=false
IS_GENERAL="true" # required for now
NEXT_PUBLIC_POSTIZ_OAUTH_DISPLAY_NAME="Authentik"
NEXT_PUBLIC_POSTIZ_OAUTH_LOGO_URL="https://raw.githubusercontent.com/walkxcode/dashboard-icons/master/png/authentik.png"
POSTIZ_GENERIC_OAUTH="false"
POSTIZ_OAUTH_URL="https://auth.example.com"
POSTIZ_OAUTH_AUTH_URL="https://auth.example.com/application/o/authorize"
POSTIZ_OAUTH_TOKEN_URL="https://auth.example.com/application/o/token"
POSTIZ_OAUTH_USERINFO_URL="https://authentik.example.com/application/o/userinfo"
POSTIZ_OAUTH_CLIENT_ID=""
POSTIZ_OAUTH_CLIENT_SECRET=""
# POSTIZ_OAUTH_SCOPE="openid profile email" # default values
# --- Farcaster (Neynar API) ---
NEYNAR_CLIENT_ID="sample_neynar_client_id"
NEYNAR_SECRET_KEY="sample_neynar_secret_key"
Expand Down
4 changes: 4 additions & 0 deletions apps/backend/src/api/api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ import { AppleProvider } from '@gitroom/backend/services/auth/providers/apple.pr
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';
import { DosMeBillingClient } from '@gitroom/nestjs-libraries/dos-billing/dos-me-billing.client';
import { DosSharedBillingService } from '@gitroom/nestjs-libraries/dos-billing/dos-shared-billing.service';
import { StripeController } from '@gitroom/backend/api/routes/stripe.controller';

const authenticatedController = [
Expand Down Expand Up @@ -112,6 +114,8 @@ const authenticatedController = [
FarcasterProvider,
WalletProvider,
OauthProvider,
DosMeBillingClient,
DosSharedBillingService,
],
get exports() {
return [...this.imports, ...this.providers];
Expand Down
65 changes: 63 additions & 2 deletions apps/backend/src/api/routes/billing.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/n
import { Request } from 'express';
import { AuthService } from '@gitroom/helpers/auth/auth.service';
import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service';
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';

Expand All @@ -28,6 +29,7 @@ export class BillingController {
private _subscriptionService: SubscriptionService,
private _notificationService: NotificationService,
private _usersService: UsersService,
private _dosBilling: DosSharedBillingService,
private _paymentService: PaymentService
) {}

Expand Down Expand Up @@ -98,6 +100,10 @@ export class BillingController {
return { blocked: true };
}

if (this._dosBilling.enabled()) {
return this.subscribeThroughDos(user, body.billing);
}

const uniqueId = req?.cookies?.track;
return (await this.provider(org)).embedded(
uniqueId,
Expand All @@ -119,6 +125,10 @@ export class BillingController {
return { blocked: true };
}

if (this._dosBilling.enabled()) {
return this.subscribeThroughDos(user, body.billing);
}

const uniqueId = req?.cookies?.track;
return (await this.provider(org)).subscribe(
uniqueId,
Expand Down Expand Up @@ -151,15 +161,34 @@ export class BillingController {
}

@Get('/portal')
async modifyPayment(@GetOrgFromRequest() org: Organization) {
async modifyPayment(
@GetOrgFromRequest() org: Organization,
@GetUserFromRequest() user: User
) {
if (this._dosBilling.enabled()) {
const { url } = await this._dosBilling.portal(
user,
process.env.FRONTEND_URL || 'https://post.crove.com'
);
return { portal: url };
}
const { url } = await (await this.provider(org)).portalLink(org.id);
return {
portal: url,
};
}

@Get('/')
getCurrentBilling(@GetOrgFromRequest() org: Organization) {
async getCurrentBilling(
@GetOrgFromRequest() org: Organization,
@GetUserFromRequest() user: User
) {
if (this._dosBilling.enabled() && user && !user.isSuperAdmin) {
try {
await this._dosBilling.syncOrg(user, org.id);
} catch {}
return this._subscriptionService.getSubscriptionByOrganizationId(org.id);
}
return this._paymentService.getSubscription(org.id);
}

Expand All @@ -176,17 +205,49 @@ export class BillingController {
user.email
);

if (this._dosBilling.enabled()) {
await this._dosBilling.cancel(user);
try {
await this._dosBilling.syncOrg(user, org.id);
} catch {}
const sub =
await this._subscriptionService.getSubscriptionByOrganizationId(org.id);
return { cancel_at: sub?.cancelAt || null };
}

return (await this.provider(org)).setToCancel(org.id);
}

@Post('/prorate')
async prorate(
@GetOrgFromRequest() org: Organization,
@GetUserFromRequest() user: User,
@Body() body: BillingSubscribeDto
) {
if (this._dosBilling.enabled()) {
return { price: body.billing === 'PRO' ? 19 : 9 };
}
return (await this.provider(org)).prorate(org.id, body);
}

private async subscribeThroughDos(user: User, billing: string) {
const result = await this._dosBilling.checkout(
user,
billing,
process.env.FRONTEND_URL || 'https://post.crove.com'
);
if ('updated' in result && result.updated) {
return {};
}
if ('url' in result && result.url) {
return { url: result.url };
}
if ('portal_url' in result && result.portal_url) {
return { portal: result.portal_url };
}
return result;
}

@Get('/charges')
async getCharges(
@GetUserFromRequest() user: User,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ConnectIntegrationDto } from '@gitroom/nestjs-libraries/dtos/integratio
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 { isCroveBillingGated } from '@gitroom/nestjs-libraries/dos-billing/crove-billing-gate';
import { ApiTags } from '@nestjs/swagger';
import { NotEnoughScopesFilter } from '@gitroom/nestjs-libraries/integrations/integration.missing.scopes';
import { AuthService } from '@gitroom/helpers/auth/auth.service';
Expand Down Expand Up @@ -194,7 +195,7 @@ export class NoAuthIntegrationsController {
}

if (
process.env.STRIPE_PUBLISHABLE_KEY &&
isCroveBillingGated() &&
org.isTrailing &&
(await this._integrationService.checkPreviousConnections(
org.id,
Expand Down
46 changes: 33 additions & 13 deletions apps/backend/src/api/routes/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import { UserAgent } from '@gitroom/nestjs-libraries/user/user.agent';
import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum';
import { TrackService } from '@gitroom/nestjs-libraries/track/track.service';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { DosSharedBillingService } from '@gitroom/nestjs-libraries/dos-billing/dos-shared-billing.service';
import { isCroveBillingGated } from '@gitroom/nestjs-libraries/dos-billing/crove-billing-gate';
import {
AuthorizationActions,
Sections,
Expand All @@ -46,7 +48,8 @@ export class UsersController {
private _authService: AuthService,
private _orgService: OrganizationService,
private _userService: UsersService,
private _trackService: TrackService
private _trackService: TrackService,
private _dosBilling: DosSharedBillingService
) {}

@Get('/chatbase-token')
Expand Down Expand Up @@ -108,28 +111,45 @@ export class UsersController {
}

const impersonate = req.cookies.impersonate || req.headers.impersonate;
const billingGated = isCroveBillingGated();
const sharedDosBilling = this._dosBilling.enabled();
let totalChannels = !billingGated
? 10000
: // @ts-ignore
organization?.subscription?.totalChannels || pricing.FREE.channel;
let tier =
// @ts-ignore
organization?.subscription?.subscriptionTier ||
(!billingGated ? 'ULTIMATE' : 'FREE');
let dosPlan: 'free' | 'plus' | 'pro' | null = null;

if (sharedDosBilling && !user.isSuperAdmin) {
try {
const mapped = await this._dosBilling.syncOrg(user, organization.id);
totalChannels = mapped.channels;
tier = mapped.tier;
dosPlan = mapped.dosPlan;
} catch {
// Fail closed to the last local subscription rather than unlock ULTIMATE.
}
}

// @ts-ignore
return {
...user,
orgId: organization.id,
totalChannels: !process.env.STRIPE_PUBLISHABLE_KEY
? 10000
: // @ts-ignore
organization?.subscription?.totalChannels || pricing.FREE.channel,
tier:
// @ts-ignore
organization?.subscription?.subscriptionTier ||
(!process.env.STRIPE_PUBLISHABLE_KEY ? 'ULTIMATE' : 'FREE'),
totalChannels,
tier,
sharedDosBilling,
dosPlan,
// @ts-ignore
role: organization?.users[0]?.role,
// @ts-ignore
isLifetime: !!organization?.subscription?.isLifetime,
admin: !!user.isSuperAdmin,
impersonate: !!impersonate,
isTrailing: !process.env.STRIPE_PUBLISHABLE_KEY
? false
: organization?.isTrailing,
allowTrial: organization?.allowTrial,
isTrailing: !billingGated ? false : organization?.isTrailing,
allowTrial: sharedDosBilling ? false : organization?.allowTrial,
streakSince: organization?.streakSince || null,
publicApi:
// @ts-ignore
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Ability, AbilityBuilder, AbilityClass } from '@casl/ability';
import { Injectable } from '@nestjs/common';
import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing';
import { isCroveBillingGated } from '@gitroom/nestjs-libraries/dos-billing/crove-billing-gate';
import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
Expand All @@ -24,7 +25,7 @@ export class PermissionsService {

const tier =
subscription?.subscriptionTier ||
(!process.env.STRIPE_PUBLISHABLE_KEY ? 'PRO' : 'FREE');
(!isCroveBillingGated() ? 'PRO' : 'FREE');

const { channel, ...all } = pricing[tier];
return {
Expand All @@ -49,7 +50,7 @@ export class PermissionsService {

if (
requestedPermission.length === 0 ||
!process.env.STRIPE_PUBLISHABLE_KEY
!isCroveBillingGated()
) {
for (const [action, section] of requestedPermission) {
can(action, section);
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/app/(app)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
plontoKey={process.env.NEXT_PUBLIC_POLOTNO!}
stripeClient={process.env.STRIPE_PUBLISHABLE_KEY!}
isChatBase={!!process.env.CHATBASE_TOKEN}
billingEnabled={!!process.env.STRIPE_PUBLISHABLE_KEY}
billingEnabled={!!process.env.STRIPE_PUBLISHABLE_KEY || process.env.DOS_SHARED_BILLING === 'true'}
discordUrl={process.env.NEXT_PUBLIC_DISCORD_SUPPORT!}
frontEndUrl={process.env.FRONTEND_URL!}
isGeneral={!!process.env.IS_GENERAL}
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/app/(extension)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
environment={process.env.NODE_ENV!}
backendUrl={process.env.NEXT_PUBLIC_BACKEND_URL!}
plontoKey={process.env.NEXT_PUBLIC_POLOTNO!}
billingEnabled={!!process.env.STRIPE_PUBLISHABLE_KEY}
billingEnabled={!!process.env.STRIPE_PUBLISHABLE_KEY || process.env.DOS_SHARED_BILLING === 'true'}
discordUrl={process.env.NEXT_PUBLIC_DISCORD_SUPPORT!}
frontEndUrl={process.env.FRONTEND_URL!}
isGeneral={!!process.env.IS_GENERAL}
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/app/(provider)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
environment={process.env.NODE_ENV!}
backendUrl={process.env.NEXT_PUBLIC_BACKEND_URL!}
plontoKey={process.env.NEXT_PUBLIC_POLOTNO!}
billingEnabled={!!process.env.STRIPE_PUBLISHABLE_KEY}
billingEnabled={!!process.env.STRIPE_PUBLISHABLE_KEY || process.env.DOS_SHARED_BILLING === 'true'}
discordUrl={process.env.NEXT_PUBLIC_DISCORD_SUPPORT!}
frontEndUrl={process.env.FRONTEND_URL!}
isGeneral={!!process.env.IS_GENERAL}
Expand Down
Loading
Loading