Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c1eabbb
fix: change user role and keep them in database
alimalek71 Dec 9, 2025
1e3d86f
style: reformat code
alimalek71 Dec 9, 2025
7abc692
fix: use url validator to stop user to attack internal network
alimalek71 Dec 9, 2025
90d9424
fix: use script and redirct sanitizer to stop some dangrous way of at…
alimalek71 Dec 9, 2025
6c75149
refactor: replace depricated redis code in ratelimit class
alimalek71 Dec 9, 2025
a5f89d0
refactor: use sanitizers
alimalek71 Dec 9, 2025
8aee195
feat: add password validor class
alimalek71 Dec 9, 2025
2eded96
feat: add coverage report
alimalek71 Dec 9, 2025
a0be2c3
fix: correct parameter order in removeWebhookLink query
alimalek71 Dec 9, 2025
a0d80ce
feat: implement retry mechanism for webhook calls
alimalek71 Dec 9, 2025
e9b9405
feat: implement soft delete for users
alimalek71 Dec 9, 2025
c2d3c6b
refactor: use environment variables for db config
alimalek71 Dec 9, 2025
93549dc
fix: improve input validation and deserialization for links resource
alimalek71 Dec 9, 2025
a85feeb
test: add security unit tests and improve script sanitizer
alimalek71 Dec 9, 2025
1744884
feat: implement audit logging system
alimalek71 Dec 9, 2025
7d34a0f
feat: add readiness and liveness health checks
alimalek71 Dec 9, 2025
c274d7b
feat: add fault tolerance and enable health checks
alimalek71 Dec 9, 2025
b4dce98
fix: resolve link creation issues and integrate audit logging
alimalek71 Dec 9, 2025
484b95a
feat(security): enforce strict URL validation and support deep links
alimalek71 Dec 9, 2025
ac1a83d
chor: update yauaa to latest version
alimalek71 Dec 9, 2025
ef7fab9
chore: start addressing security review findings
alimalek71 Dec 9, 2025
ba5ea62
fix(auth): secure role assignment and shorten token lifetime
alimalek71 Dec 9, 2025
532f27f
fix(db): remove default credentials
alimalek71 Dec 9, 2025
3c00b92
fix(links): strict custom alias creation
alimalek71 Dec 9, 2025
2015d9f
refactor(util): expose IP safety check
alimalek71 Dec 9, 2025
d720339
fix(webhook): protect against SSRF
alimalek71 Dec 9, 2025
dcc4396
fix(webhook): fix imports
alimalek71 Dec 9, 2025
2c5789b
fix(xss): sandbox script execution
alimalek71 Dec 9, 2025
16f0761
fix(xss): secure script redirect with strict context isolation
alimalek71 Dec 9, 2025
5ac7160
feat: add new record for analytic
alimalek71 Dec 9, 2025
44f4050
fix(links): treat empty hash as auto-generate request
alimalek71 Dec 9, 2025
714d023
feat(val): add NullOrNotBlank validation for hash field
alimalek71 Dec 9, 2025
9303730
feat(val): add range validation for redirect code
alimalek71 Dec 9, 2025
d6a476c
fix: resolve test failures and background job exception
alimalek71 Dec 9, 2025
a197a69
feat: enhance link validation and cleanup resources
alimalek71 Dec 9, 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
32 changes: 31 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,18 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-health</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-fault-tolerance</artifactId>
</dependency>
<dependency>
<groupId>nl.basjes.parse.useragent</groupId>
<artifactId>yauaa</artifactId>
<version>7.23.0</version>
<version>7.32.0</version>
</dependency>
<dependency>
<groupId>org.jboss.logmanager</groupId>
Expand Down Expand Up @@ -220,6 +228,9 @@
<version>${lombok.mapstruct.binding.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<compilerArg>-Amapstruct.unmappedTargetPolicy=IGNORE</compilerArg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
Expand All @@ -232,6 +243,25 @@
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
Expand Down
42 changes: 42 additions & 0 deletions src/main/java/io/shelang/aghab/domain/AuditLog.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.shelang.aghab.domain;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;

import java.time.Instant;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "audit_log")
public class AuditLog {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "user_id")
private Long userId;

@Column(name = "workspace_id")
Long workspaceId;

@Column(name = "event_type")
private String eventType;

@Column(name = "event_data")
private String eventData;

@Column(name = "ip_address")
private String ipAddress;

@CreationTimestamp
@Column(name = "create_at", nullable = false, updatable = false)
private Instant createAt;
}
6 changes: 2 additions & 4 deletions src/main/java/io/shelang/aghab/domain/LinkAnalytics.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@
import java.util.UUID;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;

import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.GenericGenerator;

@Data
@NoArgsConstructor
Expand All @@ -23,8 +22,7 @@
public class LinkAnalytics {

@Id
@GeneratedValue(generator = "UUID")
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
@org.hibernate.annotations.UuidGenerator
@Column(name = "id", updatable = false, nullable = false)
private UUID id;

Expand Down
62 changes: 30 additions & 32 deletions src/main/java/io/shelang/aghab/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,44 +29,42 @@
@Table(name = "users")
public class User {

@Id
@GeneratedValue(generator = "users_id_gen", strategy = GenerationType.SEQUENCE)
@SequenceGenerator(name = "users_id_gen", allocationSize = 1, sequenceName = "users_id_seq")
private Long id;
@Id
@GeneratedValue(generator = "users_id_gen", strategy = GenerationType.SEQUENCE)
@SequenceGenerator(name = "users_id_gen", allocationSize = 1, sequenceName = "users_id_seq")
private Long id;

private String username;
private String username;

private String password;
private String password;

private String token;
private String token;

@Column(name = "token_issue_at")
private Instant tokenIssueAt;
@Column(name = "token_issue_at")
private Instant tokenIssueAt;

@Column(name = "need_change_password")
private boolean needChangePassword;
@Column(name = "need_change_password")
private boolean needChangePassword;

@Builder.Default
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "script_user",
joinColumns = {@JoinColumn(name = "user_id")},
inverseJoinColumns = {@JoinColumn(name = "script_id")})
Set<Script> scripts = new HashSet<>();
@Builder.Default
@Column(name = "role")
private String role = "USER";

@Builder.Default
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "webhook_user",
joinColumns = {@JoinColumn(name = "user_id")},
inverseJoinColumns = {@JoinColumn(name = "webhook_id")})
Set<Webhook> webhooks = new HashSet<>();
@Builder.Default
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "script_user", joinColumns = { @JoinColumn(name = "user_id") }, inverseJoinColumns = {
@JoinColumn(name = "script_id") })
Set<Script> scripts = new HashSet<>();

@Builder.Default
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "workspace_user",
joinColumns = {@JoinColumn(name = "user_id")},
inverseJoinColumns = {@JoinColumn(name = "workspace_id")})
Set<Workspace> workspaces = new HashSet<>();
@Builder.Default
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "webhook_user", joinColumns = { @JoinColumn(name = "user_id") }, inverseJoinColumns = {
@JoinColumn(name = "webhook_id") })
Set<Webhook> webhooks = new HashSet<>();

@Builder.Default
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "workspace_user", joinColumns = { @JoinColumn(name = "user_id") }, inverseJoinColumns = {
@JoinColumn(name = "workspace_id") })
Set<Workspace> workspaces = new HashSet<>();
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package io.shelang.aghab.event.consumer;

import io.quarkus.redis.client.RedisClient;
import io.quarkus.vertx.ConsumeEvent;
import io.shelang.aghab.domain.WebhookLink;
import io.shelang.aghab.event.EventType;
import io.shelang.aghab.event.dto.WebhookCallEvent;
import io.shelang.aghab.repository.LinksRepository;
import io.shelang.aghab.repository.WebhookLinkRepository;
import io.shelang.aghab.repository.WebhookRepository;
import io.shelang.aghab.service.webhook.WebhookService;
import java.time.Instant;
import jakarta.enterprise.context.ApplicationScoped;
Expand All @@ -17,11 +15,10 @@
@ApplicationScoped
public class WebhookCallConsumer {

private static final String LOCK_EXPIRE = "100";
private static final long LOCK_EXPIRE = 100;

@SuppressWarnings("CdiInjectionPointsInspection")
@Inject
RedisClient redisClient;
io.quarkus.redis.datasource.RedisDataSource redisDataSource;
@Inject
LinksRepository linksRepository;
@Inject
Expand All @@ -33,13 +30,13 @@ public class WebhookCallConsumer {
@Transactional
public void consumer(WebhookCallEvent event) {
String key = event.getLinkId().toString();
String setNXResponse = redisClient.setnx(key, String.valueOf(Instant.now())).toString();
boolean setNXResponse = redisDataSource.value(String.class).setnx(key, String.valueOf(Instant.now()));

if (!setNXResponse.equals("1")) {
if (!setNXResponse) {
return;
}

redisClient.expire(key, LOCK_EXPIRE);
redisDataSource.key().expire(key, LOCK_EXPIRE);

webhookService.call(event.getWebhookId(), event.getLinkId(), event.getHash());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import io.shelang.aghab.repository.UserRepository;
import io.shelang.aghab.role.Roles;
import io.smallrye.jwt.auth.principal.DefaultJWTCallerPrincipal;
import java.io.IOException;
import java.time.Instant;
import jakarta.inject.Inject;
import jakarta.ws.rs.container.ContainerRequestContext;
Expand All @@ -22,8 +21,7 @@ public class APITokenAuthenticationFilter implements ContainerRequestFilter {
public void filter(ContainerRequestContext ctx) {
final SecurityContext securityContext = ctx.getSecurityContext();
if (securityContext != null) {
DefaultJWTCallerPrincipal userPrincipal =
(DefaultJWTCallerPrincipal) securityContext.getUserPrincipal();
DefaultJWTCallerPrincipal userPrincipal = (DefaultJWTCallerPrincipal) securityContext.getUserPrincipal();
if (userPrincipal == null || !securityContext.isUserInRole(Roles.API)) {
return;
}
Expand All @@ -32,9 +30,8 @@ public void filter(ContainerRequestContext ctx) {
.findByUsername(userPrincipal.getName())
.ifPresent(
user -> {
boolean before =
user.getTokenIssueAt()
.isBefore(Instant.ofEpochSecond(userPrincipal.getIssuedAtTime()));
boolean before = user.getTokenIssueAt()
.isBefore(Instant.ofEpochSecond(userPrincipal.getIssuedAtTime()));
if (!before) {
ctx.abortWith(Response.status(403).build());
}
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/io/shelang/aghab/health/AppLivenessCheck.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.shelang.aghab.health;

import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Liveness;

import jakarta.enterprise.context.ApplicationScoped;

@Liveness
@ApplicationScoped
public class AppLivenessCheck implements HealthCheck {

@Override
public HealthCheckResponse call() {
return HealthCheckResponse.up("Aghab Application is live");
}
}
57 changes: 57 additions & 0 deletions src/main/java/io/shelang/aghab/health/AppReadinessCheck.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.shelang.aghab.health;

import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.HealthCheckResponseBuilder;
import org.eclipse.microprofile.health.Readiness;

import io.quarkus.redis.client.RedisClient;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import javax.sql.DataSource;
import java.sql.Connection;

@Readiness
@ApplicationScoped
@SuppressWarnings("deprecation")
public class AppReadinessCheck implements HealthCheck {

@Inject
DataSource dataSource;

@Inject
RedisClient redisClient;

@Override
public HealthCheckResponse call() {
HealthCheckResponseBuilder responseBuilder = HealthCheckResponse.named("Aghab Application Readiness");

boolean databaseUp = checkDatabase();
boolean redisUp = checkRedis();

responseBuilder.withData("database", databaseUp ? "UP" : "DOWN")
.withData("redis", redisUp ? "UP" : "DOWN");

if (databaseUp && redisUp) {
return responseBuilder.up().build();
} else {
return responseBuilder.down().build();
}
}

private boolean checkDatabase() {
try (Connection connection = dataSource.getConnection()) {
return connection.isValid(3);
} catch (Exception e) {
return false;
}
}

private boolean checkRedis() {
try {
return redisClient.ping(java.util.Collections.emptyList()).toString().equalsIgnoreCase("PONG");
} catch (Exception e) {
return false;
}
}
}
2 changes: 1 addition & 1 deletion src/main/java/io/shelang/aghab/job/ExpireLinkJob.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public void expireLinks() {
var hasData = true;
while (hasData) {
List<LinkExpiration> expired =
linkExpirationRepository.find("expireAt < now()").page(0, 20).list();
linkExpirationRepository.find("expireAt < ?1", java.time.Instant.now()).page(0, 20).list();
if (!expired.isEmpty()) {
log.info("{} expired links fetched", expired.size());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.shelang.aghab.repository;

import io.quarkus.hibernate.orm.panache.PanacheRepository;
import io.shelang.aghab.domain.AuditLog;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class AuditLogRepository implements PanacheRepository<AuditLog> {
}
Loading