feat: login change and appsumo
This commit is contained in:
parent
681e6d4cb4
commit
0d7e54023c
16 changed files with 428 additions and 103 deletions
|
|
@ -36,6 +36,15 @@ 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,
|
||||
|
|
@ -61,6 +70,7 @@ const authenticatedController = [
|
|||
controllers: [
|
||||
RootController,
|
||||
StripeController,
|
||||
AppSumoController,
|
||||
AuthController,
|
||||
PublicController,
|
||||
MonitorController,
|
||||
|
|
@ -72,6 +82,7 @@ const authenticatedController = [
|
|||
providers: [
|
||||
AuthService,
|
||||
StripeService,
|
||||
AppSumoService,
|
||||
OpenaiService,
|
||||
ExtractContentService,
|
||||
AuthMiddleware,
|
||||
|
|
@ -82,6 +93,13 @@ const authenticatedController = [
|
|||
TrackService,
|
||||
ShortLinkService,
|
||||
Nowpayments,
|
||||
AuthProviderManager,
|
||||
GithubProvider,
|
||||
GoogleProvider,
|
||||
FarcasterProvider,
|
||||
WalletProvider,
|
||||
OauthProvider,
|
||||
AppSumoProvider,
|
||||
],
|
||||
get exports() {
|
||||
return [...this.imports, ...this.providers];
|
||||
|
|
|
|||
40
apps/backend/src/api/routes/appsumo.controller.ts
Normal file
40
apps/backend/src/api/routes/appsumo.controller.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,7 +18,8 @@ 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 (
|
||||
|
|
@ -136,7 +137,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) {
|
||||
|
|
@ -174,6 +175,12 @@ export class AuthService {
|
|||
|
||||
await NewsletterService.register(providerUser.email);
|
||||
|
||||
try {
|
||||
await providerInstance.postRegistration(body.providerToken, create.id);
|
||||
} catch (err) {
|
||||
// Don't fail registration if postRegistration fails
|
||||
}
|
||||
|
||||
return create.users[0].user;
|
||||
}
|
||||
|
||||
|
|
@ -277,16 +284,12 @@ export class AuthService {
|
|||
}
|
||||
|
||||
oauthLink(provider: string, query?: any) {
|
||||
const providerInstance = ProvidersFactory.loadProvider(
|
||||
provider as Provider
|
||||
);
|
||||
const providerInstance = this._providerManager.getProvider(provider);
|
||||
return providerInstance.generateLink(query);
|
||||
}
|
||||
|
||||
async checkExists(provider: string, code: string) {
|
||||
const providerInstance = ProvidersFactory.loadProvider(
|
||||
provider as Provider
|
||||
);
|
||||
const providerInstance = this._providerManager.getProvider(provider);
|
||||
const token = await providerInstance.getToken(code);
|
||||
const user = await providerInstance.getUser(token);
|
||||
if (!user) {
|
||||
|
|
|
|||
|
|
@ -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): 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
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
114
apps/backend/src/services/auth/providers/appsumo.provider.ts
Normal file
114
apps/backend/src/services/auth/providers/appsumo.provider.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
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}/settings`,
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
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 '';
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
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({
|
||||
|
|
@ -31,7 +33,8 @@ const clientAndYoutube = () => {
|
|||
return { client, youtube, oauth2, youtubeAnalytics };
|
||||
};
|
||||
|
||||
export class GoogleProvider implements ProvidersInterface {
|
||||
@AuthProvider({ provider: 'GOOGLE' })
|
||||
export class GoogleProvider extends AuthProviderAbstract {
|
||||
generateLink() {
|
||||
const state = 'login';
|
||||
const { client } = clientAndYoutube();
|
||||
|
|
|
|||
|
|
@ -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}`, {
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -923,6 +923,7 @@ enum Provider {
|
|||
FARCASTER
|
||||
WALLET
|
||||
GENERIC
|
||||
APPSUMO
|
||||
}
|
||||
|
||||
enum Role {
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ export class SubscriptionRepository {
|
|||
identifier: string,
|
||||
customerId: string,
|
||||
totalChannels: number,
|
||||
billing: 'STANDARD' | 'PRO',
|
||||
billing: 'STANDARD' | 'TEAM' | 'PRO' | 'ULTIMATE',
|
||||
period: 'MONTHLY' | 'YEARLY',
|
||||
cancelAt: number | null,
|
||||
code?: string,
|
||||
|
|
@ -197,6 +197,18 @@ export class SubscriptionRepository {
|
|||
}
|
||||
}
|
||||
|
||||
getSubscriptionByIdentifier(identifier: string) {
|
||||
return this._subscription.model.subscription.findFirst({
|
||||
where: {
|
||||
identifier,
|
||||
deletedAt: null,
|
||||
},
|
||||
include: {
|
||||
organization: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getSubscription(organizationId: string) {
|
||||
return this._subscription.model.subscription.findFirst({
|
||||
where: {
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export class SubscriptionService {
|
|||
async modifySubscription(
|
||||
customerId: string,
|
||||
totalChannels: number,
|
||||
billing: 'FREE' | 'STANDARD' | 'PRO'
|
||||
billing: 'FREE' | 'STANDARD' | 'TEAM' | 'PRO' | 'ULTIMATE'
|
||||
) {
|
||||
if (!customerId) {
|
||||
return false;
|
||||
|
|
@ -121,7 +121,7 @@ export class SubscriptionService {
|
|||
identifier: string,
|
||||
customerId: string,
|
||||
totalChannels: number,
|
||||
billing: 'STANDARD' | 'PRO',
|
||||
billing: 'STANDARD' | 'TEAM' | 'PRO' | 'ULTIMATE',
|
||||
period: 'MONTHLY' | 'YEARLY',
|
||||
cancelAt: number | null,
|
||||
code?: string,
|
||||
|
|
@ -154,6 +154,10 @@ export class SubscriptionService {
|
|||
);
|
||||
}
|
||||
|
||||
getSubscriptionByIdentifier(identifier: string) {
|
||||
return this._subscriptionRepository.getSubscriptionByIdentifier(identifier);
|
||||
}
|
||||
|
||||
async getSubscription(organizationId: string) {
|
||||
return this._subscriptionRepository.getSubscription(organizationId);
|
||||
}
|
||||
|
|
|
|||
110
libraries/nestjs-libraries/src/services/appsumo.service.ts
Normal file
110
libraries/nestjs-libraries/src/services/appsumo.service.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { HttpException, Injectable } from '@nestjs/common';
|
||||
import crypto from 'crypto';
|
||||
import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
|
||||
import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing';
|
||||
|
||||
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) {}
|
||||
|
||||
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 'purchase':
|
||||
case 'activate':
|
||||
return { success: true, event: payload.event };
|
||||
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 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 || subscription.organizationId
|
||||
);
|
||||
}
|
||||
|
||||
return { success: true, event: payload.event };
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue