Skip to content

Commit a8abace

Browse files
committed
Addressed pr review changes
1 parent 6f77960 commit a8abace

File tree

9 files changed

+88
-21
lines changed

9 files changed

+88
-21
lines changed

.env.example

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
NODE_ENV=development
2+
DB_PORT=5432
3+
DB_TYPE=postgres
4+
DB_HOST=db
5+
DB_USERNAME=
6+
DB_PASSWORD=
7+
DB_NAME=postgres
8+
DB_LOGGING=
9+
JWT_SECRET=
10+
JWT_VERIFICATION_TOKEN_SECRET=
11+
JWT_VERIFICATION_TOKEN_EXPIRATION_TIME=3600
12+
EMAIL_CONFIRMATION_URL=
13+
EMAIL_SERVICE=
14+
EMAIL_USER=
15+
EMAIL_PASS=
16+
ENCRYPTION_KEY=
17+
REDIRECT_BASE_URL =

src/analytics/analytics.entity.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,36 @@ import {
55
ManyToOne,
66
JoinColumn,
77
CreateDateColumn,
8+
Index,
89
} from 'typeorm';
910
import { Url } from 'src/url/url.entity';
1011

1112
@Entity({ name: 'url_analytics' })
1213
export class UrlAnalytics {
14+
@Index()
1315
@PrimaryGeneratedColumn('uuid')
1416
readonly id: string;
1517

1618
@Column({ type: 'uuid', name: 'url_id' })
1719
readonly urlId: string;
1820

1921
@Column({ type: 'varchar', length: 40, nullable: true, name: 'country' })
20-
readonly country: string;
22+
readonly country: string | null;
2123

2224
@Column({ type: 'varchar', length: 50, nullable: true, name: 'device' })
23-
readonly device: string;
25+
readonly device: string | null;
2426

2527
@Column({ type: 'varchar', length: 40, nullable: true, name: 'os' })
26-
readonly os: string;
28+
readonly os: string | null;
2729

2830
@Column({ type: 'varchar', length: 40, nullable: true, name: 'browser' })
29-
readonly browser: string;
31+
readonly browser: string | null;
3032

31-
@Column({ type: 'varchar', length: 100, nullable: true, name: 'ip' })
32-
readonly ip: string;
33+
@Column({ type: 'varchar', length: 100, nullable: true, name: 'ip_address' })
34+
readonly ipAddress: string | null;
3335

3436
@Column({ type: 'varchar', length: 255, nullable: true, name: 'user_agent' })
35-
readonly userAgent: string;
37+
readonly userAgent: string | null;
3638

3739
@CreateDateColumn({ type: 'timestamp with time zone', name: 'redirected_at' })
3840
readonly redirectedAt: Date;

src/analytics/analytics.service.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export class AnalyticsService {
1919
async recordClick(event: UrlRedirectedEvent): Promise<void> {
2020
const urlId = event.urlId;
2121
const req = event.req;
22-
const ip = (req.headers['x-forwarded-for'] as string)
22+
const ipAddress = (req.headers['x-forwarded-for'] as string)
2323
?.split(',')[0]
2424
?.trim();
2525

@@ -30,25 +30,31 @@ export class AnalyticsService {
3030
}
3131
).parse(userAgent);
3232

33+
// Match the first section inside parentheses of the User-Agent string (up to the first semicolon).
34+
// This typically represents the device or platform, e.g. "Windows NT 10.0" or "iPhone".
3335
const deviceMatch = parsed.source.match(/\(([^;]+);/);
3436
const device = deviceMatch ? deviceMatch[1] : 'Unknown Device';
3537

38+
// Look for known browser names followed by a version number,
39+
// e.g. "Chrome/120.0", "Firefox/118.0".
3640
const browserMatch = parsed.source.match(
3741
/(Chrome|Firefox|Safari|Edge|Opera)\/[\d.]+/,
3842
);
3943
const browser = browserMatch ? browserMatch[0] : 'Unknown Browser';
4044

45+
// Match the substring inside parentheses that follows the first semicolon.
46+
// For example, from "(Windows NT 10.0; Win64; x64)" → captures "Win64; x64".
4147
const osMatch = parsed.source.match(/\((?:[^;]+);\s*([^)]+)\)/);
4248

4349
const os = osMatch ? osMatch[1] : 'Unknown OS';
4450

45-
const geo = geoip.lookup(ip);
51+
const geo = geoip.lookup(ipAddress);
4652
const country = geo?.country || 'Unknown';
4753

4854
const analytics = this.analyticsRepo.create({
4955
urlId,
5056
os,
51-
ip,
57+
ipAddress,
5258
browser: browser,
5359
userAgent,
5460
device: device,
@@ -70,9 +76,13 @@ export class AnalyticsService {
7076
start: requestData.startDate,
7177
end: requestData.endDate,
7278
});
73-
} else if (requestData.startDate) {
79+
}
80+
81+
if (requestData.startDate) {
7482
qb.andWhere('a.redirectedAt >= :start', { start: requestData.startDate });
75-
} else if (requestData.endDate) {
83+
}
84+
85+
if (requestData.endDate) {
7686
qb.andWhere('a.redirectedAt <= :end', { end: requestData.endDate });
7787
}
7888

src/analytics/dto/filter-analytics-request-data.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,37 +8,37 @@ import { Transform } from 'class-transformer';
88

99
export class FilterAnalyticsRequestData {
1010
@IsOptional()
11-
browser?: string;
11+
browser?: string | null;
1212

1313
@IsOptional()
14-
device?: string;
14+
device?: string | null;
1515

1616
@IsOptional()
17-
groupByUrl?: boolean;
17+
groupByUrl?: boolean | null;
1818

1919
@IsOptional()
20-
urlId?: string;
20+
urlId?: string | null;
2121

2222
@IsOptional()
23-
os?: string;
23+
os?: string | null;
2424

2525
@IsOptional()
26-
country?: string;
26+
country?: string | null;
2727

2828
@IsOptional()
29-
ip?: string;
29+
ip?: string | null;
3030

3131
@IsOptional()
3232
@IsDateString()
3333
@Transform(({ value }) => new Date(value).toUTCString(), {
3434
toPlainOnly: true,
3535
})
36-
startDate?: Date;
36+
startDate?: Date | null;
3737

3838
@IsOptional()
3939
@IsDateString()
4040
@Transform(({ value }) => new Date(value).toUTCString(), {
4141
toPlainOnly: true,
4242
})
43-
endDate?: Date;
43+
endDate?: Date | null;
4444
}

src/auth/email-verification.entity.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import {
66
CreateDateColumn,
77
JoinColumn,
88
OneToOne,
9+
Index,
910
} from 'typeorm';
1011

1112
@Entity('email_verifications')
1213
export class EmailVerification {
14+
@Index()
1315
@PrimaryGeneratedColumn('uuid')
1416
readonly id: string;
1517

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { MigrationInterface, QueryRunner } from 'typeorm';
2+
3+
export class RenameIpColumn1762158558309 implements MigrationInterface {
4+
public async up(queryRunner: QueryRunner): Promise<void> {
5+
await queryRunner.query(`
6+
ALTER TABLE "url_analytics"
7+
RENAME COLUMN "ip" TO "ip_address";
8+
`);
9+
}
10+
11+
public async down(queryRunner: QueryRunner): Promise<void> {
12+
await queryRunner.query(`
13+
ALTER TABLE "url_analytics"
14+
RENAME COLUMN "ip_address" TO "ip";
15+
`);
16+
}
17+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { MigrationInterface, QueryRunner } from 'typeorm';
2+
3+
export class RelengthUserAgent1762159175018 implements MigrationInterface {
4+
public async up(queryRunner: QueryRunner): Promise<void> {
5+
await queryRunner.query(`
6+
ALTER TABLE "url_analytics"
7+
ALTER COLUMN "user_agent" TYPE TEXT;`);
8+
}
9+
10+
public async down(queryRunner: QueryRunner): Promise<void> {
11+
await queryRunner.query(`
12+
ALTER TABLE "url_analytics"
13+
ALTER COLUMN "user_agent" TYPE varchar(200);`);
14+
}
15+
}

src/url/url.entity.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import {
77
ManyToOne,
88
JoinColumn,
99
DeleteDateColumn,
10+
Index,
1011
} from 'typeorm';
1112
import { User } from 'src/user/user.entity';
1213

1314
@Entity({ name: 'urls' })
1415
export class Url {
16+
@Index()
1517
@PrimaryGeneratedColumn('uuid')
1618
readonly id: string;
1719

src/user/user.entity.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import {
44
Column,
55
CreateDateColumn,
66
DeleteDateColumn,
7+
Index,
78
} from 'typeorm';
89

910
@Entity({ name: 'users' })
1011
export class User {
12+
@Index()
1113
@PrimaryGeneratedColumn('uuid')
1214
readonly id: string;
1315

0 commit comments

Comments
 (0)