-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
38d51b3
commit 6e70c2d
Showing
7 changed files
with
119 additions
and
19 deletions.
There are no files selected for viewing
28 changes: 20 additions & 8 deletions
28
apps/romainlanz.com/app/redirects/controllers/process_redirect_controller.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,32 @@ | ||
import { inject } from '@adonisjs/core'; | ||
import { TimeServiceContract } from '#core/contracts/time_service_contract'; | ||
import { Visit } from '#redirects/domain/visit'; | ||
import { VisitIdentifier } from '#redirects/domain/visit_identifier'; | ||
import { RedirectRepository } from '#redirects/repositories/redirect_repository'; | ||
import { VisitRepository } from '#redirects/repositories/visit_repository'; | ||
import type { HttpContext } from '@adonisjs/core/http'; | ||
|
||
@inject() | ||
export default class ProcessRedirectController { | ||
constructor(private repository: RedirectRepository) {} | ||
constructor( | ||
private readonly redirectRepository: RedirectRepository, | ||
private readonly visitRepository: VisitRepository, | ||
private readonly timeService: TimeServiceContract | ||
) {} | ||
|
||
async execute({ params, response }: HttpContext) { | ||
const redirect = await this.repository.findByUrl(params['*']); | ||
async execute({ params, request, response }: HttpContext) { | ||
const redirect = await this.redirectRepository.findByUrl(params['*']); | ||
|
||
if (!redirect) { | ||
return response.status(404).send('Not found'); | ||
} | ||
const visit = Visit.create({ | ||
id: VisitIdentifier.generate(), | ||
createdAt: this.timeService.now(), | ||
ipAddressRaw: request.ip(), | ||
referer: request.header('referer') ?? '', | ||
redirectId: redirect.getIdentifier(), | ||
}); | ||
|
||
await this.repository.increaseVisitCount(redirect.id); | ||
await this.visitRepository.save(visit); | ||
|
||
return response.redirect(redirect.to); | ||
return response.redirect(redirect.props.destination); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { hash } from 'node:crypto'; | ||
import { Entity } from '#core/domain/entity'; | ||
import type { RedirectIdentifier } from '#redirects/domain/redirect_identifier'; | ||
import type { VisitIdentifier } from '#redirects/domain/visit_identifier'; | ||
import type { DateTime } from 'luxon'; | ||
|
||
interface Properties { | ||
id: VisitIdentifier; | ||
createdAt: DateTime; | ||
ipAddressRaw?: string; | ||
ipAddress?: string; | ||
referer: string; | ||
redirectId: RedirectIdentifier; | ||
} | ||
|
||
export class Visit extends Entity<Properties> { | ||
static create(properties: Properties) { | ||
const instance = new this(properties); | ||
|
||
if (properties.ipAddressRaw) { | ||
instance.props.ipAddress = hash( | ||
'md5', | ||
`${properties.ipAddressRaw}${properties.createdAt.toFormat('yyyy-MM-dd')}` | ||
); | ||
} | ||
|
||
return instance; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { Identifier } from '#core/domain/identifier'; | ||
|
||
export class VisitIdentifier extends Identifier<'VisitIdentifier'> {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
26 changes: 26 additions & 0 deletions
26
apps/romainlanz.com/app/redirects/repositories/visit_repository.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { assertExists } from '@adonisjs/core/helpers/assert'; | ||
import { db } from '#core/services/db'; | ||
import { sql } from 'kysely'; | ||
import type { Visit } from '#redirects/domain/visit'; | ||
|
||
export class VisitRepository { | ||
save(visit: Visit) { | ||
assertExists(visit.props.ipAddress, 'IP address is required'); | ||
|
||
return db | ||
.insertInto('visits') | ||
.values({ | ||
id: visit.getIdentifier().toString(), | ||
created_at: visit.props.createdAt.toJSDate(), | ||
ip_address: visit.props.ipAddress, | ||
referer: visit.props.referer, | ||
redirect_id: visit.props.redirectId.toString(), | ||
}) | ||
.onConflict((builder) => | ||
builder.column('ip_address').doUpdateSet({ | ||
count: sql`visits.count + 1`, | ||
}) | ||
) | ||
.execute(); | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
apps/romainlanz.com/database/migrations/1734206783527_create_visits_table.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import type { Kysely } from 'kysely'; | ||
|
||
export async function up(db: Kysely<any>): Promise<void> { | ||
await db.schema | ||
.createTable('visits') | ||
.addColumn('id', 'uuid', (col) => col.primaryKey()) | ||
.addColumn('created_at', 'timestamptz', (col) => col.notNull()) | ||
.addColumn('ip_address', 'uuid', (col) => col.unique().notNull()) | ||
.addColumn('referer', 'text') | ||
.addColumn('redirect_id', 'uuid', (col) => col.notNull()) | ||
.addColumn('count', 'int4', (col) => col.notNull().defaultTo(1)) | ||
.execute(); | ||
|
||
await db.schema.createIndex('fk_visits_redirect_id').on('visits').column('redirect_id').execute(); | ||
} | ||
|
||
export async function down(db: Kysely<any>): Promise<void> { | ||
await db.schema.dropTable('visits').execute(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters