Deposit
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('short_links')
|
||||
export class ShortLink {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ length: 12 })
|
||||
@Index({ unique: true })
|
||||
code: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
targetUrl: string;
|
||||
|
||||
@Column({ type: 'int', default: 0 })
|
||||
clickCount: number;
|
||||
|
||||
@Column({ type: 'timestamp', nullable: true })
|
||||
expiresAt: Date | null;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Controller, Get, Param, Res } from '@nestjs/common';
|
||||
import { ApiExcludeController } from '@nestjs/swagger';
|
||||
import type { Response } from 'express';
|
||||
import { Public } from '../auth/decorators/public.decorator';
|
||||
import { ShortLinksService } from './short-links.service';
|
||||
|
||||
/**
|
||||
* Registered outside the global `api` prefix (see main.ts) so links stay short,
|
||||
* e.g. `https://api.example.com/s/aB3xQ9k`.
|
||||
*/
|
||||
@ApiExcludeController()
|
||||
@Controller('s')
|
||||
export class ShortLinksController {
|
||||
constructor(private readonly shortLinksService: ShortLinksService) {}
|
||||
|
||||
@Public()
|
||||
@Get(':code')
|
||||
async redirect(@Param('code') code: string, @Res() res: Response) {
|
||||
const targetUrl = await this.shortLinksService.resolveAndTrack(code);
|
||||
return res.redirect(targetUrl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ShortLink } from './entities/short-link.entity';
|
||||
import { ShortLinksController } from './short-links.controller';
|
||||
import { ShortLinksService } from './short-links.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([ShortLink])],
|
||||
controllers: [ShortLinksController],
|
||||
providers: [ShortLinksService],
|
||||
exports: [ShortLinksService],
|
||||
})
|
||||
export class ShortLinksModule {}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { GoneException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ShortLink } from './entities/short-link.entity';
|
||||
|
||||
const CODE_ALPHABET =
|
||||
'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
const CODE_LENGTH = 7;
|
||||
const MAX_GENERATION_ATTEMPTS = 5;
|
||||
|
||||
@Injectable()
|
||||
export class ShortLinksService {
|
||||
private readonly apiBaseUrl: string;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ShortLink)
|
||||
private readonly repository: Repository<ShortLink>,
|
||||
private readonly configService: ConfigService,
|
||||
) {
|
||||
this.apiBaseUrl = (
|
||||
this.configService.get<string>('API_PUBLIC_URL') ??
|
||||
'http://localhost:4000'
|
||||
).replace(/\/$/, '');
|
||||
}
|
||||
|
||||
/** Creates a short link and returns its public, shareable URL. */
|
||||
async createShortUrl(
|
||||
targetUrl: string,
|
||||
expiresAt: Date | null = null,
|
||||
): Promise<string> {
|
||||
const code = await this.generateUniqueCode();
|
||||
await this.repository.save(
|
||||
this.repository.create({ code, targetUrl, expiresAt }),
|
||||
);
|
||||
|
||||
return `${this.apiBaseUrl}/s/${code}`;
|
||||
}
|
||||
|
||||
async resolveAndTrack(code: string): Promise<string> {
|
||||
const shortLink = await this.repository.findOne({ where: { code } });
|
||||
if (!shortLink) {
|
||||
throw new NotFoundException('لینک یافت نشد');
|
||||
}
|
||||
|
||||
if (shortLink.expiresAt && shortLink.expiresAt.getTime() < Date.now()) {
|
||||
throw new GoneException('این لینک منقضی شده است');
|
||||
}
|
||||
|
||||
shortLink.clickCount += 1;
|
||||
await this.repository.save(shortLink);
|
||||
|
||||
return shortLink.targetUrl;
|
||||
}
|
||||
|
||||
private async generateUniqueCode(): Promise<string> {
|
||||
for (let attempt = 0; attempt < MAX_GENERATION_ATTEMPTS; attempt += 1) {
|
||||
const code = this.generateCode();
|
||||
const exists = await this.repository.existsBy({ code });
|
||||
if (!exists) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Failed to generate a unique short link code');
|
||||
}
|
||||
|
||||
private generateCode(): string {
|
||||
const bytes = randomBytes(CODE_LENGTH);
|
||||
let code = '';
|
||||
for (let i = 0; i < CODE_LENGTH; i += 1) {
|
||||
code += CODE_ALPHABET[bytes[i] % CODE_ALPHABET.length];
|
||||
}
|
||||
return code;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user