Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a757684
feat(user): add UTM parameters support in user creation and sign-up
Dobrunia Aug 6, 2025
ece94f6
Bump version up to 1.1.30
github-actions[bot] Aug 6, 2025
a4a7903
docs(user): clarify UTM parameter description in user creation and si…
Dobrunia Aug 6, 2025
9ab01e5
chore(deps): update @hawk.so/types to version 0.1.33 and enhance user…
Dobrunia Aug 6, 2025
33087ee
feat(user): validate and sanitize UTM parameters during user creation
Dobrunia Aug 6, 2025
14dc023
feat(analytics): enhance UTM parameter validation to include object t…
Dobrunia Aug 6, 2025
31ff97d
refactor(analytics): improve readability of UTM parameter validation …
Dobrunia Aug 6, 2025
e6a179f
refactor(utm): move UTM parameter validation and sanitization to a de…
Dobrunia Aug 9, 2025
c4aceeb
refactor(utm): add tests for UTM validation
Dobrunia Aug 9, 2025
bd6d64a
refactor(utm): extract UTM key validation and character checks into c…
Dobrunia Aug 12, 2025
02376ca
refactor(utm): define maximum UTM value length as a constant for bett…
Dobrunia Aug 12, 2025
e8114da
refactor(utm): enhance UTM parameter validation to return detailed re…
Dobrunia Aug 13, 2025
64b7122
chore(package): bump version to 1.1.32
Dobrunia Aug 13, 2025
6ff0955
Merge branch 'master' into feat/utm-tags-integration
Dobrunia Aug 13, 2025
c9e37d7
fix lint
Dobrunia Aug 13, 2025
4c6496e
fix lint
Dobrunia Aug 13, 2025
7febc31
refactor(utm): streamline UTM parameter sanitization and improve erro…
Dobrunia Aug 13, 2025
5d3ee35
test(utm): update UTM parameter validation tests to check for valid a…
Dobrunia Aug 13, 2025
e67f815
refactor(utm): simplify UTM parameter handling by removing sanitizati…
Dobrunia Aug 13, 2025
271e380
fix
Dobrunia Aug 13, 2025
34ea138
feat(users): add conditional UTM parameter inclusion in user data and…
Dobrunia Aug 13, 2025
61a95fd
test(utm): update validation tests to return undefined for null and n…
Dobrunia Aug 13, 2025
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hawk.api",
"version": "1.1.29",
"version": "1.1.30",
"main": "index.ts",
"license": "UNLICENSED",
"scripts": {
Expand Down Expand Up @@ -37,7 +37,7 @@
"@graphql-tools/schema": "^8.5.1",
"@graphql-tools/utils": "^8.9.0",
"@hawk.so/nodejs": "^3.1.1",
"@hawk.so/types": "^0.1.31",
"@hawk.so/types": "^0.1.33",
"@types/amqp-connection-manager": "^2.0.4",
"@types/bson": "^4.0.5",
"@types/debug": "^4.1.5",
Expand Down
7 changes: 6 additions & 1 deletion src/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,12 @@ export default class UserModel extends AbstractModel<UserDBScheme> implements Us
/**
* Saved bank cards for one-click payments
*/
public bankCards?: BankCard[]
public bankCards?: BankCard[];

/**
* UTM parameters from signup - Data form where user went to sign up. Used for analytics purposes
*/
public utm?: UserDBScheme['utm'];

/**
* Model's collection
Expand Down
18 changes: 15 additions & 3 deletions src/models/usersFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Collection, Db } from 'mongodb';
import DataLoaders from '../dataLoaders';
import { UserDBScheme } from '@hawk.so/types';
import { Analytics, AnalyticsEventTypes } from '../utils/analytics';
import { sanitizeUtmParams, validateUtmParams } from '../utils/utm/utm';

/**
* Users factory to work with User Model
Expand Down Expand Up @@ -62,16 +63,27 @@ export default class UsersFactory extends AbstractModelFactory<UserDBScheme, Use
* Creates new user in DB and returns it
* @param email - user email
* @param password - user password
* @param utm - Data form where user went to sign up. Used for analytics purposes
*/
public async create(email: string, password?: string): Promise<UserModel> {
const generatedPassword = password || await UserModel.generatePassword();
public async create(
email: string,
password?: string,
utm?: UserDBScheme['utm']
): Promise<UserModel> {
const generatedPassword = password || (await UserModel.generatePassword());
const hashedPassword = await UserModel.hashPassword(generatedPassword);

const userData = {
const userData: Partial<UserDBScheme> = {
email,
password: hashedPassword,
notifications: UserModel.generateDefaultNotificationsSettings(email),
};

if (validateUtmParams(utm)) {
const sanitizedUtm = sanitizeUtmParams(utm);
userData.utm = sanitizedUtm;
}

const userId = (await this.collection.insertOne(userData)).insertedId;

const user = new UserModel({
Expand Down
13 changes: 11 additions & 2 deletions src/resolvers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { dateFromObjectId } from '../utils/dates';
import { UserDBScheme } from '@hawk.so/types';
import * as telegram from '../utils/telegram';
import { MongoError } from 'mongodb';
import { validateUtmParams, sanitizeUtmParams } from '../utils/utm/utm';

/**
* See all types and fields here {@see ../typeDefs/user.graphql}
Expand All @@ -37,17 +38,25 @@ export default {
* Register user with provided email
* @param _obj - parent object (undefined for this resolver)
* @param email - user email
* @param utm - Data form where user went to sign up. Used for analytics purposes
* @param factories - factories for working with models
*/
async signUp(
_obj: undefined,
{ email }: {email: string},
{ email, utm }: { email: string; utm?: UserDBScheme['utm'] },
{ factories }: ResolverContextBase
): Promise<boolean | string> {
// Validate and sanitize UTM parameters
if (!validateUtmParams(utm)) {
throw new UserInputError('Invalid UTM parameters provided');
}

const sanitizedUtm = sanitizeUtmParams(utm);

let user;

try {
user = await factories.usersFactory.create(email);
user = await factories.usersFactory.create(email, undefined, sanitizedUtm);

const password = user.generatedPassword!;

Expand Down
35 changes: 35 additions & 0 deletions src/typeDefs/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,36 @@ import { gql } from 'apollo-server-express';
import isE2E from '../utils/isE2E';

export default gql`
"""
UTM parameters input type
"""
input UtmInput {
"""
UTM source
"""
source: String

"""
UTM medium
"""
medium: String

"""
UTM campaign
"""
campaign: String

"""
UTM content
"""
content: String

"""
UTM term
"""
term: String
}

"""
Authentication token
"""
Expand Down Expand Up @@ -72,6 +102,11 @@ export default gql`
Registration email
"""
email: String! @validate(isEmail: true)

"""
UTM parameters
"""
utm: UtmInput
): ${isE2E ? 'String!' : 'Boolean!'}

"""
Expand Down
82 changes: 82 additions & 0 deletions src/utils/utm/utm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { UserDBScheme } from '@hawk.so/types';

/**
* Validates UTM parameters
* @param utm - Data form where user went to sign up. Used for analytics purposes
* @returns boolean - true if valid, false if invalid
*/
export function validateUtmParams(utm: UserDBScheme['utm']): boolean {
if (!utm) {
return true;
}

// Check if utm is an object
if (typeof utm !== 'object' || Array.isArray(utm)) {
return false;
}

const utmKeys = ['source', 'medium', 'campaign', 'content', 'term'];
const providedKeys = Object.keys(utm);

// Check if utm object is not empty
if (providedKeys.length === 0) {
return true; // Empty object is valid
}

// Check if all provided keys are valid UTM keys
const hasInvalidKeys = providedKeys.some((key) => !utmKeys.includes(key));
if (hasInvalidKeys) {
return false;
}

// Check if values are strings and not too long
for (const [key, value] of Object.entries(utm)) {
if (value !== undefined && value !== null) {
if (typeof value !== 'string') {
return false;
}

// Check length
if (value.length === 0 || value.length > 200) {
return false;
}

// Check for valid characters - only allow alphanumeric, spaces, hyphens, underscores, dots
if (!/^[a-zA-Z0-9\s\-_.]+$/.test(value)) {
return false;
}
}
}

return true;
}

/**
* Sanitizes UTM parameters by removing invalid characters
* @param utm - Data form where user went to sign up. Used for analytics purposes
* @returns sanitized UTM parameters or undefined if invalid
*/
export function sanitizeUtmParams(utm: UserDBScheme['utm']): UserDBScheme['utm'] {
if (!utm) {
return undefined;
}

const utmKeys = ['source', 'medium', 'campaign', 'content', 'term'];
const sanitized: UserDBScheme['utm'] = {};

for (const [key, value] of Object.entries(utm)) {
if (utmKeys.includes(key) && value && typeof value === 'string') {
// Sanitize value: keep only allowed characters and limit length
const cleanValue = value
.replace(/[^a-zA-Z0-9\s\-_.]/g, '')
.trim()
.substring(0, 200);

if (cleanValue.length > 0) {
(sanitized as any)[key] = cleanValue;
}
}
}

return Object.keys(sanitized).length > 0 ? sanitized : undefined;
}
Loading
Loading