feat: remove logic and add logic to enterprise

This commit is contained in:
Nevo David 2026-03-05 12:03:06 +07:00
parent 78b834d18f
commit 748e2190e9
8 changed files with 41 additions and 337 deletions

View file

@ -1,11 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

View file

@ -36,15 +36,12 @@ import { EnterpriseController } from '@gitroom/backend/api/routes/enterprise.con
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 { AppSumoController } from '@gitroom/backend/api/routes/appsumo.controller';
import { AppSumoService } from '@gitroom/nestjs-libraries/services/appsumo.service';
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';
import { AppSumoProvider } from '@gitroom/backend/services/auth/providers/appsumo.provider';
const authenticatedController = [
UsersController,
@ -70,7 +67,6 @@ const authenticatedController = [
controllers: [
RootController,
StripeController,
AppSumoController,
AuthController,
PublicController,
MonitorController,
@ -82,7 +78,6 @@ const authenticatedController = [
providers: [
AuthService,
StripeService,
AppSumoService,
OpenaiService,
ExtractContentService,
AuthMiddleware,
@ -99,7 +94,6 @@ const authenticatedController = [
FarcasterProvider,
WalletProvider,
OauthProvider,
AppSumoProvider,
],
get exports() {
return [...this.imports, ...this.providers];

View file

@ -1,40 +0,0 @@
import {
Controller,
HttpException,
Post,
RawBodyRequest,
Req,
} from '@nestjs/common';
import { AppSumoService } from '@gitroom/nestjs-libraries/services/appsumo.service';
import { ApiTags } from '@nestjs/swagger';
@ApiTags('AppSumo')
@Controller('/appsumo')
export class AppSumoController {
constructor(private readonly _appSumoService: AppSumoService) {}
@Post('/')
async webhook(@Req() req: RawBodyRequest<Request>) {
// @ts-ignore
const timestamp = req.headers['x-appsumo-timestamp'] as string;
// @ts-ignore
const signature = req.headers['x-appsumo-signature'] as string;
if (!timestamp || !signature) {
throw new HttpException('Missing signature headers', 401);
}
this._appSumoService.validateSignature(
req.rawBody!,
timestamp,
signature
);
try {
const payload = JSON.parse(req.rawBody!.toString());
return this._appSumoService.handleWebhook(payload);
} catch (e) {
throw new HttpException(e, 500);
}
}
}

View file

@ -4,13 +4,17 @@ 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 _organizationService: OrganizationService,
private _integrationService: IntegrationService,
private _postsService: PostsService
) {}
@Post('/create-user')
@ -86,4 +90,39 @@ export class EnterpriseController {
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

@ -1,114 +0,0 @@
import {
AuthProvider,
AuthProviderAbstract,
} from '@gitroom/backend/services/auth/providers.interface';
import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing';
const APPSUMO_OPENID_BASE = 'https://appsumo.com/openid';
const APPSUMO_TIER_MAP: Record<number, 'STANDARD' | 'TEAM' | 'PRO'> = {
1: 'STANDARD',
2: 'TEAM',
3: 'PRO',
};
@AuthProvider({ provider: 'APPSUMO' })
export class AppSumoProvider extends AuthProviderAbstract {
constructor(private _subscriptionService: SubscriptionService) {
super();
}
generateLink() {
const params = new URLSearchParams({
client_id: process.env.APPSUMO_CLIENT_ID!,
redirect_uri: `${process.env.FRONTEND_URL}/integrations/social/appsumo`,
response_type: 'code',
scope: 'openid',
});
return `${APPSUMO_OPENID_BASE}/authorize/?${params.toString()}`;
}
async getToken(code: string) {
const clientId = process.env.APPSUMO_CLIENT_ID;
const clientSecret = process.env.APPSUMO_CLIENT_SECRET;
if (!clientId || !clientSecret) {
return '';
}
const tokenResponse = await fetch(`${APPSUMO_OPENID_BASE}/token/`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: clientId,
client_secret: clientSecret,
code,
redirect_uri: `${process.env.FRONTEND_URL}/settings`,
}),
});
if (!tokenResponse.ok) {
return '';
}
const { access_token } = await tokenResponse.json();
return access_token || '';
}
async getUser(accessToken: string) {
if (!accessToken) {
return { id: '', email: '' };
}
const response = await fetch(
`${APPSUMO_OPENID_BASE}/license_key/?access_token=${accessToken}`,
);
if (!response.ok) {
return { id: '', email: '' };
}
const data = await response.json();
if (!data.license_key) {
return { id: '', email: '' };
}
return {
id: String(`appsumo_${data.license_key}`),
email: String(`appsumo_${data.license_key}`),
};
}
async postRegistration(accessToken: string, orgId: string) {
if (!accessToken) {
return;
}
const response = await fetch(
`${APPSUMO_OPENID_BASE}/license_key/?access_token=${accessToken}`,
);
if (!response.ok) {
return;
}
const data = await response.json();
const billing = APPSUMO_TIER_MAP[data.tier] || 'STANDARD';
await this._subscriptionService.createOrUpdateSubscription(
false,
data.license_key,
data.license_key,
pricing[billing].channel!,
billing,
'YEARLY',
null,
data.license_key,
orgId
);
}
}

View file

@ -44,7 +44,6 @@ export async function middleware(request: NextRequest) {
}
if (
nextUrl.href.indexOf('appsumo') === -1 &&
nextUrl.pathname.startsWith('/integrations/social/') &&
nextUrl.href.indexOf('state=login') === -1
) {
@ -74,7 +73,7 @@ export async function middleware(request: NextRequest) {
const org = nextUrl.searchParams.get('org');
const url = new URL(nextUrl).search;
if (!nextUrl.pathname.startsWith('/auth') && !authCookie) {
const providers = ['google', 'appsumo', 'settings'];
const providers = ['google', 'settings'];
const findIndex = providers.find((p) => nextUrl.href.indexOf(p) > -1);
const additional = !findIndex
? ''

View file

@ -923,7 +923,6 @@ enum Provider {
FARCASTER
WALLET
GENERIC
APPSUMO
}
enum Role {

View file

@ -1,162 +0,0 @@
import { HttpException, Injectable } from '@nestjs/common';
import crypto from 'crypto';
import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service';
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing';
import { Provider } from '@prisma/client';
const APPSUMO_TIER_MAP: Record<number, 'STANDARD' | 'TEAM' | 'PRO'> = {
1: 'STANDARD',
2: 'TEAM',
3: 'PRO',
};
export interface AppSumoWebhookPayload {
license_key: string;
event:
| 'purchase'
| 'activate'
| 'upgrade'
| 'downgrade'
| 'deactivate'
| 'migrate';
license_status: string;
event_timestamp: number;
created_at: number;
tier: number;
prev_license_key?: string;
test?: boolean;
}
@Injectable()
export class AppSumoService {
constructor(
private _subscriptionService: SubscriptionService,
private _usersService: UsersService,
private _organizationService: OrganizationService
) {}
validateSignature(rawBody: Buffer, timestamp: string, signature: string) {
const apiKey = process.env.APPSUMO_API_KEY;
if (!apiKey) {
throw new HttpException('AppSumo API key not configured', 500);
}
const message = timestamp + rawBody.toString();
const expectedSignature = crypto
.createHmac('sha256', apiKey)
.update(message)
.digest('hex');
const isValid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
if (!isValid) {
throw new HttpException('Invalid signature', 401);
}
}
async handleWebhook(payload: AppSumoWebhookPayload) {
if (payload.test) {
return { success: true, event: payload.event };
}
switch (payload.event) {
case 'activate':
return this.handlePurchaseOrActivate(payload);
case 'upgrade':
return this.handleUpgradeOrDowngrade(payload);
case 'downgrade':
return this.handleUpgradeOrDowngrade(payload);
case 'deactivate':
return this.handleDeactivate(payload);
default:
return { success: true, event: payload.event };
}
}
private async handlePurchaseOrActivate(payload: AppSumoWebhookPayload) {
const orgId = await this.findOrgByLicenseKey(payload.license_key);
if (!orgId) {
return { success: true, event: payload.event };
}
const billing = APPSUMO_TIER_MAP[payload.tier] || 'STANDARD';
await this._subscriptionService.createOrUpdateSubscription(
false,
payload.license_key,
payload.license_key,
pricing[billing].channel!,
billing,
'YEARLY',
null,
payload.license_key,
orgId
);
return { success: true, event: payload.event };
}
private async findOrgByLicenseKey(licenseKey: string) {
const subscription =
await this._subscriptionService.getSubscriptionByIdentifier(licenseKey);
if (subscription) {
return subscription.organizationId;
}
const user = await this._usersService.getUserByProvider(
`appsumo_${licenseKey}`,
Provider.APPSUMO
);
if (!user) {
return null;
}
const orgs = await this._organizationService.getOrgsByUserId(user.id);
return orgs[0]?.id || null;
}
private async handleUpgradeOrDowngrade(payload: AppSumoWebhookPayload) {
const identifier = payload.prev_license_key || payload.license_key;
const subscription =
await this._subscriptionService.getSubscriptionByIdentifier(identifier);
if (subscription) {
const billing = APPSUMO_TIER_MAP[payload.tier] || 'STANDARD';
await this._subscriptionService.createOrUpdateSubscription(
false,
payload.license_key,
payload.license_key,
pricing[billing].channel!,
billing,
'YEARLY',
null,
payload.license_key,
subscription.organizationId
);
}
return { success: true, event: payload.event };
}
private async handleDeactivate(payload: AppSumoWebhookPayload) {
const subscription =
await this._subscriptionService.getSubscriptionByIdentifier(
payload.license_key
);
if (subscription) {
await this._subscriptionService.deleteSubscription(
subscription.organization.paymentId
);
}
return { success: true, event: payload.event };
}
}