feat: fbp + fclib
This commit is contained in:
parent
3d185ad688
commit
2d0c47d46e
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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] : []),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
export enum TrackEnum {
|
||||
ViewContent = 0,
|
||||
CompleteRegistration = 1,
|
||||
InitiateCheckout = 2,
|
||||
StartTrial = 3,
|
||||
Purchase = 4,
|
||||
}
|
||||
|
|
@ -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'];
|
||||
},
|
||||
);
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue