Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
5df2ae2
WIP: url
laxman-aqw Oct 14, 2025
5c1d3a8
merged feat-url with fix-migration
laxman-aqw Oct 14, 2025
4b50054
WIP: url
laxman-aqw Oct 15, 2025
c5c5cbe
Merge branch 'feat-throttler' into feat-url
laxman-aqw Oct 15, 2025
2920e5e
Add: Created url table migration
laxman-aqw Oct 15, 2025
e2c4ae7
MERGE: merge with feat-throttler branch
laxman-aqw Oct 15, 2025
67a8e87
FIX: removed unnecessary logs
laxman-aqw Oct 15, 2025
2ecc34c
WIP: add url modules
laxman-aqw Oct 17, 2025
7dbdae6
merged with email-verification branch
laxman-aqw Oct 24, 2025
fbd42cd
add original_url column in url table to store original hash url and c…
laxman-aqw Oct 24, 2025
a324f39
add url crud with guard
laxman-aqw Oct 27, 2025
6ca753e
Revert "add url crud with guard"
laxman-aqw Oct 27, 2025
a21f934
Reapply "add url crud with guard"
laxman-aqw Oct 27, 2025
fb4a74f
removed: unnecessary codes
laxman-aqw Oct 27, 2025
5f50cdc
feat(cron): added cron to send url expiry alert mail to the user
laxman-aqw Oct 27, 2025
8c48d53
feat(url_analytics): create urlAnalytics table migration
laxman-aqw Oct 27, 2025
36aca8e
feat(analytics): added geoip-lite to extract geo datas
laxman-aqw Oct 28, 2025
01914ff
FIX: add browser and device column in analytics table
laxman-aqw Oct 28, 2025
e479d79
Merge branch 'feat-analytics-table' into feat-analytics
laxman-aqw Oct 28, 2025
34e1f24
Fix: add browser and device in entity
laxman-aqw Oct 28, 2025
c6b0e6b
FIX: addressed pr review changes
laxman-aqw Oct 28, 2025
f451d88
Merge branch 'feat-cron' into feat-analytics-table
laxman-aqw Oct 28, 2025
83ab959
Merge branch 'feat-analytics-table' into feat-analytics
laxman-aqw Oct 28, 2025
d87cc85
feat(analytics): add service to record url analytics details
laxman-aqw Oct 28, 2025
788b212
remove: removed unnecessary codes
laxman-aqw Oct 28, 2025
7a39610
update service
laxman-aqw Oct 28, 2025
d00d3d5
Merge branch 'feat-throttler' into feat-url
laxman-aqw Oct 29, 2025
f77d386
Merge pull request #12 from pagevamp/feat-analytics-table
laxman-aqw Oct 29, 2025
1994097
Merge pull request #11 from pagevamp/feat-cron
laxman-aqw Oct 29, 2025
0cf4e7b
Merge branch 'feat-cron' of github.com:pagevamp/laxman-url-shortner i…
laxman-aqw Oct 29, 2025
59873e9
Merge branch 'feat-cron' into feat-url
laxman-aqw Oct 29, 2025
b5f6d1a
Merge branch 'feat-url' of github.com:pagevamp/laxman-url-shortner in…
laxman-aqw Oct 29, 2025
869b52f
Addressed pr review changes: validated userid at authguard
laxman-aqw Oct 29, 2025
20b17d7
FIX: removed consoles and used error handler method to handle errors
laxman-aqw Oct 29, 2025
3bdafe2
FIX: Add userId in checking existing url query to avoid duplicate query
laxman-aqw Oct 29, 2025
d49fb89
FIX: removed unnecessary try catch blocks
laxman-aqw Oct 29, 2025
41c0dd0
removed an unnecessary migration file
laxman-aqw Oct 30, 2025
72fb87a
fix: addressed pr review changes
laxman-aqw Oct 30, 2025
bff9f3f
feat(analytics): updated types and analytic service
laxman-aqw Oct 30, 2025
57a3c0b
feat(url): add redirect url request data
laxman-aqw Oct 31, 2025
8aba362
Addressed PR review changes, updated auth service to handle only sign…
laxman-aqw Oct 31, 2025
9afd588
FIX: renamed GetUrlRequestData to GetUrlResponseData
laxman-aqw Oct 31, 2025
7a6bf9c
used decorator to set http status code 302
laxman-aqw Oct 31, 2025
d83efe2
FIX: PR review changes
laxman-aqw Oct 31, 2025
39276c3
fix: addressed pr review changes
laxman-aqw Oct 31, 2025
7df70b4
FIX: Addressed PR review changes
laxman-aqw Oct 31, 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
13 changes: 11 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,28 @@
"@nestjs-modules/mailer": "^2.0.2",
"@nestjs/common": "^11.0.1",
"@nestjs/core": "^11.0.1",
"@nestjs/event-emitter": "^3.0.1",
"@nestjs/jwt": "^11.0.0",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/schedule": "^6.0.1",
"@nestjs/throttler": "^6.4.0",
"@nestjs/typeorm": "^11.0.0",
"bcrypt": "^6.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"err-code": "^3.0.1",
"fast-geoip": "^1.1.88",
"geoip-lite": "^1.4.10",
"nodemailer": "^7.0.9",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pg": "^8.16.3",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"typeorm": "^0.3.27"
"typeorm": "^0.3.27",
"ua-parser-js": "^2.0.6",
"useragent": "^2.3.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
Expand All @@ -51,10 +58,12 @@
"@nestjs/testing": "^11.0.1",
"@types/bcrypt": "^6.0.0",
"@types/express": "^5.0.0",
"@types/geoip-lite": "^1.4.4",
"@types/jest": "^30.0.0",
"@types/node": "^22.10.7",
"@types/nodemailer": "^7.0.2",
"@types/supertest": "^6.0.2",
"@types/useragent": "^2.3.4",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
Expand Down Expand Up @@ -87,5 +96,5 @@
"coverageDirectory": "../coverage",
"testEnvironment": "node"
},
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
"packageManager": "pnpm@10.19.0+sha512.c9fc7236e92adf5c8af42fd5bf1612df99c2ceb62f27047032f4720b33f8eacdde311865e91c411f2774f618d82f320808ecb51718bfa82c060c4ba7c76a32b8"
}
513 changes: 513 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions src/analytics/analytics.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Controller } from '@nestjs/common';
import { AnalyticsService } from './analytics.service';

@Controller('analytics')
export class AnalyticsController {
constructor(private readonly analyticsService: AnalyticsService) {}
}
40 changes: 40 additions & 0 deletions src/analytics/analytics.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
JoinColumn,
CreateDateColumn,
} from 'typeorm';
import { Url } from 'src/url/url.entity';

@Entity({ name: 'url_analytics' })
export class UrlAnalytics {
@PrimaryGeneratedColumn('uuid')
readonly id: string;

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

@Column({ type: 'varchar', length: 40, nullable: true, name: 'country' })
readonly country: string;

@Column({ type: 'varchar', length: 50, nullable: true, name: 'device' })
readonly device: string;

@Column({ type: 'varchar', length: 40, nullable: true, name: 'browser' })
readonly browser: string;

@Column({ type: 'varchar', length: 100, nullable: true, name: 'ip' })
readonly ip: string;

@Column({ type: 'varchar', length: 255, nullable: true, name: 'user_agent' })
readonly userAgent: string;

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

@ManyToOne(() => Url, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'url_id' })
readonly url: Url;
}
14 changes: 14 additions & 0 deletions src/analytics/analytics.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AnalyticsService } from './analytics.service';
import { UrlAnalytics } from './analytics.entity';
import { AnalyticsController } from './analytics.controller';
import { AuthModule } from 'src/auth/auth.module';

@Module({
imports: [TypeOrmModule.forFeature([UrlAnalytics]), AuthModule],
providers: [AnalyticsService],
exports: [AnalyticsService],
controllers: [AnalyticsController],
})
export class AnalyticsModule {}
50 changes: 50 additions & 0 deletions src/analytics/analytics.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Injectable } from '@nestjs/common';
import { Request } from 'express';
import geoip from 'geoip-lite';
import { UrlAnalytics } from './analytics.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import useragent from 'useragent';
import { ParsedUserAgent } from './types';
@Injectable()
export class AnalyticsService {
constructor(
@InjectRepository(UrlAnalytics)
private readonly analyticsRepo: Repository<UrlAnalytics>,
) {}

async recordClick(urlId: string, req: Request): Promise<void> {
const ip =
(req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() ||
req.socket?.remoteAddress?.replace('::ffff:', '') ||
'0.0.0.0';

const userAgent = req.headers['user-agent'] || '';
const parsed = (
useragent as unknown as {
parse: (ua?: string, jsAgent?: string) => ParsedUserAgent;
}
).parse(userAgent);

const deviceMatch = parsed.source.match(/\(([^;]+);/);
const device = deviceMatch ? deviceMatch[1] : 'Unknown Device';

const browserMatch = parsed.source.match(
/(Chrome|Firefox|Safari|Edge|Opera)\/[\d.]+/,
);
const browser = browserMatch ? browserMatch[0] : 'Unknown Browser';

const geo = geoip.lookup(ip);
const country = geo?.country || 'Unknown';
const analytics = this.analyticsRepo.create({
urlId,
ip,
browser: browser,
userAgent,
device: device,
country,
});

await this.analyticsRepo.save(analytics);
}
}
7 changes: 7 additions & 0 deletions src/analytics/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type ParsedUserAgent = {
family: string;
major: string;
minor: string;
patch: string;
source: string;
};
13 changes: 12 additions & 1 deletion src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,27 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { UserModule } from './user/user.module';
import { AuthModule } from './auth/auth.module';
import { EmailModule } from './email/email.module';
import { UrlModule } from './url/url.module';
import dataSource from './data-source';
import { ScheduleModule } from '@nestjs/schedule';
import { CronModule } from './cron/cron.module';
import { AnalyticsModule } from './analytics/analytics.module';
import { GuardService } from './guard/guard.service';
import { GuardModule } from './guard/guard.module';

@Module({
imports: [
ScheduleModule.forRoot(),
TypeOrmModule.forRoot(dataSource.options),
UserModule,
AuthModule,
EmailModule,
UrlModule,
CronModule,
AnalyticsModule,
GuardModule,
],
controllers: [AppController],
providers: [],
providers: [GuardService],
})
export class AppModule {}
6 changes: 4 additions & 2 deletions src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import { AuthController } from './auth.controller';
import { JwtModule } from '@nestjs/jwt';
import { CryptoService } from './crypto.service';
import { EmailModule } from 'src/email/email.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { EmailVerification } from './email-verification.entity';
import { User } from 'src/user/user.entity';
import { EmailVerification } from 'src/auth/email-verification.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserModule } from 'src/user/user.module';

@Module({
imports: [
TypeOrmModule.forFeature([User, EmailVerification]),
EmailModule,
UserModule,
JwtModule.register({
global: true,
secret: process.env.JWT_SECRET,
Expand Down
Loading