feat: fbp + fclib

This commit is contained in:
Nevo David 2024-12-16 00:09:55 +07:00
parent 3d185ad688
commit 2d0c47d46e
14 changed files with 571 additions and 514 deletions

View File

@ -26,6 +26,7 @@ import { CopilotController } from '@gitroom/backend/api/routes/copilot.controlle
import { AgenciesController } from '@gitroom/backend/api/routes/agencies.controller';
import { PublicController } from '@gitroom/backend/api/routes/public.controller';
import { RootController } from '@gitroom/backend/api/routes/root.controller';
import { TrackService } from '@gitroom/nestjs-libraries/track/track.service';
const authenticatedController = [
UsersController,
@ -63,6 +64,7 @@ const authenticatedController = [
PermissionsService,
CodesService,
IntegrationManager,
TrackService,
],
get exports() {
return [...this.imports, ...this.providers];

View File

@ -1,4 +1,13 @@
import { Body, Controller, Get, Param, Post, Req, Res } from '@nestjs/common';
import {
Body,
Controller,
Get,
Ip,
Param,
Post,
Req,
Res,
} from '@nestjs/common';
import { Response, Request } from 'express';
import { CreateOrgUserDto } from '@gitroom/nestjs-libraries/dtos/auth/create.org.user.dto';
@ -9,6 +18,8 @@ import { ForgotPasswordDto } from '@gitroom/nestjs-libraries/dtos/auth/forgot.pa
import { ApiTags } from '@nestjs/swagger';
import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management';
import { EmailService } from '@gitroom/nestjs-libraries/services/email.service';
import { RealIP } from 'nestjs-real-ip';
import { UserAgent } from '@gitroom/nestjs-libraries/user/user.agent';
@ApiTags('Auth')
@Controller('/auth')
@ -21,7 +32,9 @@ export class AuthController {
async register(
@Req() req: Request,
@Body() body: CreateOrgUserDto,
@Res({ passthrough: true }) response: Response
@Res({ passthrough: true }) response: Response,
@RealIP() ip: string,
@UserAgent() userAgent: string
) {
try {
const getOrgFromCookie = this._authService.getOrgFromCookie(
@ -31,10 +44,13 @@ export class AuthController {
const { jwt, addedOrg } = await this._authService.routeAuth(
body.provider,
body,
ip,
userAgent,
getOrgFromCookie
);
const activationRequired = body.provider === 'LOCAL' && this._emailService.hasProvider();
const activationRequired =
body.provider === 'LOCAL' && this._emailService.hasProvider();
if (activationRequired) {
response.header('activate', 'true');
@ -73,7 +89,9 @@ export class AuthController {
async login(
@Req() req: Request,
@Body() body: LoginUserDto,
@Res({ passthrough: true }) response: Response
@Res({ passthrough: true }) response: Response,
@RealIP() ip: string,
@UserAgent() userAgent: string
) {
try {
const getOrgFromCookie = this._authService.getOrgFromCookie(
@ -83,6 +101,8 @@ export class AuthController {
const { jwt, addedOrg } = await this._authService.routeAuth(
body.provider,
body,
ip,
userAgent,
getOrgFromCookie
);

View File

@ -1,11 +1,23 @@
import { Controller, Get, Param } from '@nestjs/common';
import { Body, Controller, Get, Param, Post, Req, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AgenciesService } from '@gitroom/nestjs-libraries/database/prisma/agencies/agencies.service';
import { TrackService } from '@gitroom/nestjs-libraries/track/track.service';
import { RealIP } from 'nestjs-real-ip';
import { UserAgent } from '@gitroom/nestjs-libraries/user/user.agent';
import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum';
import { Request, Response } from 'express';
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
import { User } from '@prisma/client';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management';
@ApiTags('Public')
@Controller('/public')
export class PublicController {
constructor(private _agenciesService: AgenciesService) {}
constructor(
private _agenciesService: AgenciesService,
private _trackService: TrackService
) {}
@Get('/agencies-list')
async getAgencyByUser() {
return this._agenciesService.getAllAgencies();
@ -17,9 +29,7 @@ export class PublicController {
}
@Get('/agencies-information/:agency')
async getAgencyInformation(
@Param('agency') agency: string,
) {
async getAgencyInformation(@Param('agency') agency: string) {
return this._agenciesService.getAgencyInformation(agency);
}
@ -27,4 +37,53 @@ export class PublicController {
async getAgenciesCount() {
return this._agenciesService.getCount();
}
@Post('/t')
async trackEvent(
@Res() res: Response,
@Req() req: Request,
@RealIP() ip: string,
@UserAgent() userAgent: string,
@Body()
body: { fbclid?: string; tt: TrackEnum; additional: Record<string, any> }
) {
const uniqueId = req?.cookies?.track || makeId(10);
console.log(
req?.cookies?.track,
ip,
userAgent,
body.tt,
body.additional,
body.fbclid
);
await this._trackService.track(
req?.cookies?.track,
ip,
userAgent,
body.tt,
body.additional,
body.fbclid
);
if (!req.cookies.track) {
res.cookie('track', uniqueId, {
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
sameSite: 'none',
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
});
}
if (body.fbclid && !req.cookies.fbclid) {
res.cookie('fbclid', body.fbclid, {
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
sameSite: 'none',
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
});
}
res.status(200).send();
}
}

View File

@ -27,6 +27,11 @@ import { ApiTags } from '@nestjs/swagger';
import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service';
import { UserDetailDto } from '@gitroom/nestjs-libraries/dtos/users/user.details.dto';
import { HttpForbiddenException } from '@gitroom/nestjs-libraries/services/exception.filter';
import { RealIP } from 'nestjs-real-ip';
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';
@ApiTags('User')
@Controller('/user')
@ -36,7 +41,8 @@ export class UsersController {
private _stripeService: StripeService,
private _authService: AuthService,
private _orgService: OrganizationService,
private _userService: UsersService
private _userService: UsersService,
private _trackService: TrackService
) {}
@Get('/self')
async getSelf(
@ -54,7 +60,8 @@ export class UsersController {
// @ts-ignore
totalChannels: organization?.subscription?.totalChannels || pricing.FREE.channel,
// @ts-ignore
tier: organization?.subscription?.subscriptionTier || (!process.env.STRIPE_PUBLISHABLE_KEY ? 'ULTIMATE' : 'FREE'),
tier: organization?.subscription?.subscriptionTier ||
(!process.env.STRIPE_PUBLISHABLE_KEY ? 'ULTIMATE' : 'FREE'),
// @ts-ignore
role: organization?.users[0]?.role,
// @ts-ignore
@ -62,7 +69,9 @@ export class UsersController {
admin: !!user.isSuperAdmin,
impersonate: !!req.cookies.impersonate,
// @ts-ignore
publicApi: (organization?.users[0]?.role === 'SUPERADMIN' || organization?.users[0]?.role === 'ADMIN') ? organization?.apiKey : '',
publicApi: organization?.users[0]?.role === 'SUPERADMIN' || organization?.users[0]?.role === 'ADMIN'
? organization?.apiKey
: '',
};
}
@ -205,4 +214,28 @@ export class UsersController {
response.status(200).send();
}
@Post('/t')
async trackEvent(
@Res({ passthrough: true }) res: Response,
@Req() req: Request,
@GetUserFromRequest() user: User,
@RealIP() ip: string,
@UserAgent() userAgent: string,
@Body() body: { tt: TrackEnum; additional: Record<string, any> }
) {
const uniqueId = req?.cookies?.track || makeId(10);
await this._trackService.track(req?.cookies?.track, ip, userAgent, body.tt, body.additional, null, user);
if (!req.cookies.track) {
res.cookie('track', uniqueId, {
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
sameSite: 'none',
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
});
}
res.status(200).send();
}
}

View File

@ -17,6 +17,7 @@ async function bootstrap() {
credentials: true,
exposedHeaders: ['reload', 'onboarding', 'activate'],
origin: [
'http://localhost:3001',
process.env.FRONTEND_URL,
...(process.env.MAIN_URL ? [process.env.MAIN_URL] : []),
],

View File

@ -18,11 +18,13 @@ export class AuthService {
private _userService: UsersService,
private _organizationService: OrganizationService,
private _notificationService: NotificationService,
private _emailService: EmailService,
private _emailService: EmailService
) {}
async routeAuth(
provider: Provider,
body: CreateOrgUserDto | LoginUserDto,
ip: string,
userAgent: string,
addToOrg?: boolean | { orgId: string; role: 'USER' | 'ADMIN'; id: string }
) {
if (provider === Provider.LOCAL) {
@ -32,7 +34,11 @@ export class AuthService {
throw new Error('User already exists');
}
const create = await this._organizationService.createOrgAndUser(body);
const create = await this._organizationService.createOrgAndUser(
body,
ip,
userAgent
);
const addedOrg =
addToOrg && typeof addToOrg !== 'boolean'
@ -45,7 +51,11 @@ export class AuthService {
: false;
const obj = { addedOrg, jwt: await this.jwt(create.users[0].user) };
await this._emailService.sendEmail(body.email, 'Activate your account', `Click <a href="${process.env.FRONTEND_URL}/auth/activate/${obj.jwt}">here</a> to activate your account`);
await this._emailService.sendEmail(
body.email,
'Activate your account',
`Click <a href="${process.env.FRONTEND_URL}/auth/activate/${obj.jwt}">here</a> to activate your account`
);
return obj;
}
@ -62,7 +72,9 @@ export class AuthService {
const user = await this.loginOrRegisterProvider(
provider,
body as CreateOrgUserDto
body as CreateOrgUserDto,
ip,
userAgent
);
const addedOrg =
@ -101,7 +113,9 @@ export class AuthService {
private async loginOrRegisterProvider(
provider: Provider,
body: CreateOrgUserDto
body: CreateOrgUserDto,
ip: string,
userAgent: string
) {
const providerInstance = ProvidersFactory.loadProvider(provider);
const providerUser = await providerInstance.getUser(body.providerToken);
@ -118,15 +132,19 @@ export class AuthService {
return user;
}
const create = await this._organizationService.createOrgAndUser({
company: body.company,
email: providerUser.email,
password: '',
provider,
providerId: providerUser.id,
});
const create = await this._organizationService.createOrgAndUser(
{
company: body.company,
email: providerUser.email,
password: '',
provider,
providerId: providerUser.id,
},
ip,
userAgent
);
NewsletterService.register(providerUser.email);
await NewsletterService.register(providerUser.email);
return create.users[0].user;
}
@ -162,7 +180,11 @@ export class AuthService {
}
async activate(code: string) {
const user = AuthChecker.verifyJWT(code) as { id: string, activated: boolean, email: string };
const user = AuthChecker.verifyJWT(code) as {
id: string;
activated: boolean;
email: string;
};
if (user.id && !user.activated) {
const getUserAgain = await this._userService.getUserByEmail(user.email);
if (getUserAgain.activated) {

View File

@ -207,7 +207,9 @@ export class OrganizationRepository {
async createOrgAndUser(
body: Omit<CreateOrgUserDto, 'providerToken'> & { providerId?: string },
hasEmail: boolean
hasEmail: boolean,
ip: string,
userAgent: string
) {
return this._organization.model.organization.create({
data: {
@ -226,6 +228,8 @@ export class OrganizationRepository {
providerName: body.provider,
providerId: body.providerId || '',
timezone: 0,
ip,
agent: userAgent,
},
},
},

View File

@ -15,11 +15,15 @@ export class OrganizationService {
private _notificationsService: NotificationService
) {}
async createOrgAndUser(
body: Omit<CreateOrgUserDto, 'providerToken'> & { providerId?: string }
body: Omit<CreateOrgUserDto, 'providerToken'> & { providerId?: string },
ip: string,
userAgent: string
) {
return this._organizationRepository.createOrgAndUser(
body,
this._notificationsService.hasEmailProvider()
this._notificationsService.hasEmailProvider(),
ip,
userAgent
);
}

View File

@ -30,8 +30,8 @@ model Organization {
buyerOrganization MessagesGroup[]
usedCodes UsedCodes[]
credits Credits[]
plugs Plugs[]
customers Customer[]
plugs Plugs[]
customers Customer[]
}
model User {
@ -66,6 +66,8 @@ model User {
payoutProblems PayoutProblems[]
lastOnline DateTime @default(now())
agencies SocialMediaAgency[]
ip String?
agent String?
@@unique([email, providerName])
@@index([lastReadNotifications])

View File

@ -0,0 +1,80 @@
import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum';
import { User } from '@prisma/client';
import { Injectable } from '@nestjs/common';
import {
ServerEvent,
EventRequest,
UserData,
CustomData,
FacebookAdsApi,
} from 'facebook-nodejs-business-sdk';
import { createHash } from 'crypto';
const access_token = process.env.FACEBOOK_PIXEL_ACCESS_TOKEN!;
const pixel_id = process.env.NEXT_PUBLIC_FACEBOOK_PIXEL!;
if (access_token && pixel_id) {
FacebookAdsApi.init(access_token || '');
}
@Injectable()
export class TrackService {
private hashValue(value: string) {
return createHash('sha256').update(value).digest('hex');
}
track(
uniqueId: string,
ip: string,
agent: string,
tt: TrackEnum,
additional: Record<string, any>,
fbclid?: string,
user?: User
) {
if (!access_token || !pixel_id) {
return;
}
// @ts-ignore
const current_timestamp = Math.floor(new Date() / 1000);
const userData = new UserData();
userData.setClientIpAddress(user?.ip || ip);
userData.setClientUserAgent(user?.agent || agent);
if (fbclid) {
userData.setFbc(fbclid);
}
if (user && user.email) {
userData.setEmails([this.hashValue(user.email)]);
}
let customData = null;
if (additional?.value) {
customData = new CustomData();
customData.setValue(additional.value).setCurrency('USD');
}
const serverEvent = new ServerEvent()
.setEventName(TrackEnum[tt])
.setEventTime(current_timestamp)
.setActionSource('website');
if (user && user.id) {
serverEvent.setEventId(uniqueId || user.id);
}
if (userData) {
serverEvent.setUserData(userData);
}
if (customData) {
serverEvent.setCustomData(customData);
}
const eventsData = [serverEvent];
const eventRequest = new EventRequest(access_token, pixel_id).setEvents(
eventsData
);
return eventRequest.execute();
}
}

View File

@ -0,0 +1,7 @@
export enum TrackEnum {
ViewContent = 0,
CompleteRegistration = 1,
InitiateCheckout = 2,
StartTrial = 3,
Purchase = 4,
}

View File

@ -0,0 +1,8 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const UserAgent = createParamDecorator(
(data: unknown, ctx: ExecutionContext): string => {
const request = ctx.switchToHttp().getRequest();
return request.headers['user-agent'];
},
);

778
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -68,6 +68,7 @@
"@sweetalert2/theme-dark": "^5.0.16",
"@types/bcrypt": "^5.0.2",
"@types/concat-stream": "^2.0.3",
"@types/facebook-nodejs-business-sdk": "^20.0.2",
"@types/jsonwebtoken": "^9.0.5",
"@types/lodash": "^4.14.202",
"@types/md5": "^2.3.5",
@ -105,6 +106,7 @@
"copy-to-clipboard": "^3.3.3",
"crypto-hash": "^3.0.0",
"dayjs": "^1.11.10",
"facebook-nodejs-business-sdk": "^21.0.5",
"google-auth-library": "^9.11.0",
"googleapis": "^137.1.0",
"ioredis": "^5.3.2",
@ -116,6 +118,7 @@
"mime-types": "^2.1.35",
"multer": "^1.4.5-lts.1",
"nestjs-command": "^3.1.4",
"nestjs-real-ip": "^3.0.1",
"next": "^14.2.14",
"next-plausible": "^3.12.0",
"nodemailer": "^6.9.15",