Skip to content

Commit 74e1c76

Browse files
authored
fix: allow only query my own data (#126)
* fix: allow access only my own data Signed-off-by: Matheus Cruz <[email protected]> * fix: fix compilation Signed-off-by: Matheus Cruz <[email protected]> --------- Signed-off-by: Matheus Cruz <[email protected]>
1 parent 79f810c commit 74e1c76

24 files changed

+213
-79
lines changed

site/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ <h2 class="text-3xl font-bold mb-6">Timeless is Open Source</h2>
100100
<h2 class="text-3xl font-bold mb-6">Start Using Timeless Today</h2>
101101
<p class="text-lg mb-8 max-w-2xl mx-auto">Your financial Life organized forever. No effort required.</p>
102102
<a href="#"
103-
class="bg-white text-blue-600 px-6 py-3 rounded-xl text-lg font-semibold hover:bg-gray-200 transition">Join the
104-
Waitlist</a>
103+
class="bg-white text-blue-600 px-6 py-3 rounded-xl text-lg font-semibold hover:bg-gray-200 transition">Register
104+
Now</a>
105105
</section>
106106

107107
<!-- Footer -->
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
package dev.matheuscruz.domain;
22

33
import io.quarkus.hibernate.orm.panache.PanacheRepository;
4+
import io.quarkus.logging.Log;
5+
import io.quarkus.panache.common.Parameters;
46
import jakarta.enterprise.context.ApplicationScoped;
57
import java.util.List;
68

79
@ApplicationScoped
810
public class RecordRepository implements PanacheRepository<Record> {
911

10-
public List<AmountAndTypeOnly> getRecordsWithAmountAndTypeOnly() {
11-
return find("select r.amount, r.transaction from Record as r").project(AmountAndTypeOnly.class).list();
12+
public List<AmountAndTypeOnly> getRecordsWithAmountAndTypeOnlyByUser(String userId) {
13+
Log.info("Getting balance for user ID: " + userId);
14+
return find("select r.amount, r.transaction from Record as r where userId = :userId",
15+
Parameters.with("userId", userId)).project(AmountAndTypeOnly.class).list();
1216
}
17+
1318
}

timeless-api/src/main/java/dev/matheuscruz/infra/ai/TextAiService.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dev.matheuscruz.infra.ai;
22

33
import dev.langchain4j.service.UserMessage;
4+
import dev.langchain4j.service.V;
45
import dev.matheuscruz.infra.ai.data.AllRecognizedOperations;
56
import dev.matheuscruz.infra.ai.tools.GetBalanceTool;
67
import io.quarkiverse.langchain4j.RegisterAiService;
@@ -10,9 +11,12 @@ public interface TextAiService {
1011

1112
@UserMessage("""
1213
You are a smart financial assistant capable of performing two types of operations based on the user's message:
14+
1315
1. Extracting financial transaction data.
1416
2. Responding with the user's current account balance using available tools.
1517
18+
The user ID is {{userId}}.
19+
1620
Your task is to analyze the content between the --- delimiters and return a JSON object in the following format:
1721
1822
{
@@ -80,6 +84,6 @@ public interface TextAiService {
8084
{message}
8185
---
8286
""")
83-
AllRecognizedOperations handleMessage(String message);
87+
AllRecognizedOperations handleMessage(String message, @V("userId") String userId);
8488

8589
}

timeless-api/src/main/java/dev/matheuscruz/infra/ai/tools/GetBalanceTool.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ public GetBalanceTool(RecordRepository recordRepository) {
1717
this.recordRepository = recordRepository;
1818
}
1919

20-
@Tool(value = "get account balance")
21-
public BigDecimal getBalance() {
22-
List<AmountAndTypeOnly> list = this.recordRepository.getRecordsWithAmountAndTypeOnly();
20+
@Tool(value = "get account balance for a user")
21+
public BigDecimal getBalance(String userId) {
22+
List<AmountAndTypeOnly> list = this.recordRepository.getRecordsWithAmountAndTypeOnlyByUser(userId);
2323
return list.stream()
2424
.map(record -> record.getTransaction().equals(Transactions.OUT)
2525
? record.getAmount().multiply(new BigDecimal(-1))

timeless-api/src/main/java/dev/matheuscruz/infra/queue/SQS.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ private void processMessage(String body, String receiptHandle) {
7777

7878
private void handleUserMessage(User user, IncomingMessage message, String receiptHandle) {
7979
try {
80-
AllRecognizedOperations allRecognizedOperations = aiService.handleMessage(message.messageBody());
80+
AllRecognizedOperations allRecognizedOperations = aiService.handleMessage(message.messageBody(),
81+
user.getId());
8182

8283
for (RecognizedOperation recognizedOperation : allRecognizedOperations.all()) {
8384
switch (recognizedOperation.operation()) {

timeless-api/src/main/java/dev/matheuscruz/presentation/MessageResource.java

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package dev.matheuscruz.presentation;
22

3-
import com.fasterxml.jackson.databind.ObjectMapper;
43
import dev.langchain4j.data.image.Image;
54
import dev.matheuscruz.domain.Record;
65
import dev.matheuscruz.domain.RecordRepository;
@@ -11,9 +10,10 @@
1110
import dev.matheuscruz.infra.ai.data.AiOperations;
1211
import dev.matheuscruz.infra.ai.data.RecognizedOperation;
1312
import dev.matheuscruz.infra.ai.data.RecognizedTransaction;
13+
import dev.matheuscruz.presentation.data.ImageRequest;
14+
import dev.matheuscruz.presentation.data.MessageRequest;
1415
import io.quarkus.narayana.jta.QuarkusTransaction;
1516
import jakarta.validation.Valid;
16-
import jakarta.validation.constraints.NotBlank;
1717
import jakarta.ws.rs.Consumes;
1818
import jakarta.ws.rs.NotFoundException;
1919
import jakarta.ws.rs.POST;
@@ -25,16 +25,24 @@
2525
import java.util.function.Predicate;
2626
import java.util.stream.Stream;
2727

28+
/**
29+
* We allow only whatsapp clients to send messages to this endpoint.
30+
* <p>
31+
* While we do not have everything implemented in an asynchronous way without the communication between whatsapp app and
32+
* our backend, we will block this endpoint to only whatsapp IP.
33+
* <p>
34+
* It is a block on network level, configured in the infrastructure layer.
35+
*/
2836
@Path("/api/messages")
2937
public class MessageResource {
3038

31-
private final UserRepository userRepository;
32-
private final TextAiService aiService;
33-
private final ImageAiService imageAiService;
34-
private final RecordRepository recordRepository;
39+
final UserRepository userRepository;
40+
final TextAiService aiService;
41+
final ImageAiService imageAiService;
42+
final RecordRepository recordRepository;
3543

3644
public MessageResource(TextAiService aiService, ImageAiService imageAiService, RecordRepository recordRepository,
37-
ObjectMapper mapper, UserRepository userRepository) {
45+
UserRepository userRepository) {
3846
this.aiService = aiService;
3947
this.imageAiService = imageAiService;
4048
this.recordRepository = recordRepository;
@@ -76,15 +84,15 @@ public Response image(@Valid ImageRequest req) {
7684
}
7785

7886
private Response handleMessage(User user, String message) {
79-
List<RecognizedOperation> response = aiService.handleMessage(message).all();
87+
List<RecognizedOperation> response = aiService.handleMessage(message, user.getId()).all();
8088
return processOnlyAddTransaction(user, response);
8189
}
8290

8391
private Response processOnlyAddTransaction(User user, List<RecognizedOperation> messages) {
8492

8593
List<RecognizedTransaction> onlyAddTransaction = messages.stream()
8694
.filter(message -> AiOperations.ADD_TRANSACTION.equals(message.operation()))
87-
.map(recognizedOperation -> recognizedOperation.recognizedTransaction()).toList();
95+
.map(RecognizedOperation::recognizedTransaction).toList();
8896

8997
handleTransactions(onlyAddTransaction, user);
9098

@@ -101,10 +109,4 @@ private void handleTransactions(List<RecognizedTransaction> transactions, User u
101109
recordRepository.persist(recordStream);
102110
});
103111
}
104-
105-
public record MessageRequest(@NotBlank String from, @NotBlank String message) {
106-
}
107-
108-
public record ImageRequest(@NotBlank String from, @NotBlank String base64, String text, @NotBlank String mimeType) {
109-
}
110112
}
Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
package dev.matheuscruz.presentation;
22

3-
import dev.matheuscruz.domain.*;
3+
import dev.matheuscruz.domain.AmountAndTypeOnly;
44
import dev.matheuscruz.domain.Record;
5+
import dev.matheuscruz.domain.RecordRepository;
6+
import dev.matheuscruz.domain.Transactions;
7+
import dev.matheuscruz.domain.User;
8+
import dev.matheuscruz.domain.UserRepository;
9+
import dev.matheuscruz.presentation.data.CreateRecordRequest;
10+
import dev.matheuscruz.presentation.data.PageRecord;
11+
import dev.matheuscruz.presentation.data.RecordItemResponse;
512
import io.quarkus.narayana.jta.QuarkusTransaction;
613
import io.quarkus.panache.common.Page;
714
import io.quarkus.panache.common.Parameters;
8-
import jakarta.transaction.Transactional;
15+
import io.quarkus.security.Authenticated;
16+
import jakarta.enterprise.context.RequestScoped;
917
import jakarta.validation.Valid;
10-
import jakarta.validation.constraints.NotBlank;
11-
import jakarta.validation.constraints.NotNull;
12-
import jakarta.validation.constraints.PositiveOrZero;
1318
import jakarta.ws.rs.DELETE;
1419
import jakarta.ws.rs.ForbiddenException;
1520
import jakarta.ws.rs.GET;
@@ -19,22 +24,26 @@
1924
import jakarta.ws.rs.core.Response;
2025
import java.math.BigDecimal;
2126
import java.net.URI;
22-
import java.time.Instant;
23-
import java.time.LocalDateTime;
2427
import java.time.ZoneId;
25-
import java.time.ZoneOffset;
2628
import java.time.format.DateTimeFormatter;
2729
import java.util.List;
2830
import java.util.Optional;
31+
import org.eclipse.microprofile.jwt.Claim;
32+
import org.eclipse.microprofile.jwt.Claims;
2933
import org.jboss.resteasy.reactive.RestQuery;
3034

35+
@RequestScoped
3136
@Path("/api/records")
37+
@Authenticated
3238
public class RecordResource {
3339

40+
static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
41+
42+
@Claim(standard = Claims.upn)
43+
String upn;
44+
3445
RecordRepository recordRepository;
3546
UserRepository userRepository;
36-
static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
37-
static Instant INSTANT_2025 = LocalDateTime.of(2025, 1, 1, 0, 0, 0).toInstant(ZoneOffset.UTC);
3847

3948
public RecordResource(RecordRepository recordRepository, UserRepository userRepository) {
4049
this.recordRepository = recordRepository;
@@ -43,9 +52,9 @@ public RecordResource(RecordRepository recordRepository, UserRepository userRepo
4352

4453
@DELETE
4554
@Path("/{id}")
46-
@Transactional
4755
public Response delete(@PathParam("id") Long id) {
48-
recordRepository.deleteById(id);
56+
QuarkusTransaction.requiringNew().run(() -> recordRepository.delete("id = :id AND userId = :userId",
57+
Parameters.with("id", id).and("userId", upn)));
4958
return Response.status(Response.Status.NO_CONTENT).build();
5059
}
5160

@@ -56,8 +65,11 @@ public Response createRecord(@Valid CreateRecordRequest req) {
5665
Record record = new Record.Builder().userId(user.getId()).amount(req.amount()).description(req.description())
5766
.transaction(req.transaction()).category(req.category()).build();
5867

59-
QuarkusTransaction.requiringNew().run(() -> this.recordRepository.persist(record));
68+
if (!user.getId().equals(upn)) {
69+
return Response.status(Response.Status.FORBIDDEN).build();
70+
}
6071

72+
QuarkusTransaction.requiringNew().run(() -> this.recordRepository.persist(record));
6173
return Response.created(URI.create("/api/records/" + record.getId())).build();
6274
}
6375

@@ -67,40 +79,29 @@ public Response getRecords(@RestQuery("page") String p, @RestQuery("limit") Stri
6779
int page = Integer.parseInt(Optional.of(p).orElse("0"));
6880
int limit = Integer.parseInt(Optional.of(l).orElse("10"));
6981

70-
long totalRecords = recordRepository.count();
82+
// TODO: https://github.com/mcruzdev/timeless/issues/125
83+
long totalRecords = recordRepository.count("userId = :userId", Parameters.with("userId", upn));
7184

72-
List<RecordItemResponse> output = recordRepository.findAll().page(Page.of(page, limit)).list().stream()
73-
.map(record -> {
85+
// pagination
86+
List<RecordItemResponse> output = recordRepository.find("userId = :userId", Parameters.with("userId", upn))
87+
.page(Page.of(page, limit)).list().stream().map(record -> {
7488
String format = record.getCreatedAt().atZone(ZoneId.of("America/Sao_Paulo")).toLocalDate()
7589
.format(formatter);
7690
return new RecordItemResponse(record.getId(), record.getAmount(), record.getDescription(),
7791
record.getTransaction().name(), format, record.getCategory().name());
7892
}).toList();
7993

80-
List<Record> list = recordRepository.find("createdAt >= :instant AND createdAt <= :now",
81-
Parameters.with("instant", INSTANT_2025).and("now", Instant.now())).list();
82-
83-
Optional<BigDecimal> totalExpenses = list.stream()
84-
.filter(item -> item.getTransaction().equals(Transactions.OUT)).map(Record::getAmount)
94+
// calculate total expenses and total in
95+
List<AmountAndTypeOnly> amountAndType = recordRepository.getRecordsWithAmountAndTypeOnlyByUser(upn);
96+
Optional<BigDecimal> totalExpenses = amountAndType.stream()
97+
.filter(item -> item.getTransaction().equals(Transactions.OUT)).map(AmountAndTypeOnly::getAmount)
8598
.reduce(BigDecimal::add);
8699

87-
Optional<BigDecimal> totalIn = list.stream().filter(item -> item.getTransaction().equals(Transactions.IN))
88-
.map(Record::getAmount).reduce(BigDecimal::add);
100+
Optional<BigDecimal> totalIn = amountAndType.stream()
101+
.filter(item -> item.getTransaction().equals(Transactions.IN)).map(AmountAndTypeOnly::getAmount)
102+
.reduce(BigDecimal::add);
89103

90-
return Response.ok(new PagedRecord(output, totalRecords, totalExpenses.orElse(BigDecimal.ZERO),
104+
return Response.ok(new PageRecord(output, totalRecords, totalExpenses.orElse(BigDecimal.ZERO),
91105
totalIn.orElse(BigDecimal.ZERO))).build();
92106
}
93-
94-
public record PagedRecord(List<RecordItemResponse> items, Long totalRecords, BigDecimal totalExpenses,
95-
BigDecimal totalIn) {
96-
}
97-
98-
public record RecordItemResponse(Long id, BigDecimal amount, String description, String transaction,
99-
String createdAt, String category) {
100-
}
101-
102-
public record CreateRecordRequest(@PositiveOrZero BigDecimal amount, @NotBlank String description,
103-
@NotNull Transactions transaction, @NotBlank String from, @NotNull Categories category) {
104-
}
105-
106107
}

timeless-api/src/main/java/dev/matheuscruz/presentation/SignInResource.java

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
import dev.matheuscruz.domain.UserRepository;
55
import dev.matheuscruz.infra.security.BCryptAdapter;
66
import dev.matheuscruz.infra.security.Groups;
7+
import dev.matheuscruz.presentation.data.SignInRequest;
8+
import dev.matheuscruz.presentation.data.SignInResponse;
79
import io.quarkus.panache.common.Parameters;
810
import io.smallrye.jwt.build.Jwt;
11+
import jakarta.annotation.security.PermitAll;
912
import jakarta.validation.Valid;
10-
import jakarta.validation.constraints.Email;
11-
import jakarta.validation.constraints.NotBlank;
12-
import jakarta.validation.constraints.Size;
1313
import jakarta.ws.rs.ForbiddenException;
1414
import jakarta.ws.rs.POST;
1515
import jakarta.ws.rs.Path;
@@ -18,6 +18,7 @@
1818
import java.util.Set;
1919

2020
@Path("/api/sign-in")
21+
@PermitAll
2122
public class SignInResource {
2223

2324
UserRepository userRepository;
@@ -44,9 +45,4 @@ public Response signIn(@Valid SignInRequest req) {
4445
return Response.ok(new SignInResponse(token, user.getId(), user.fullName(), req.email())).build();
4546
}
4647

47-
public record SignInRequest(@Email String email, @NotBlank @Size(min = 8, max = 32) String password) {
48-
}
49-
50-
public record SignInResponse(String token, String id, String name, String email) {
51-
}
5248
}

timeless-api/src/main/java/dev/matheuscruz/presentation/SignUpResource.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
import dev.matheuscruz.domain.UserRepository;
55
import dev.matheuscruz.infra.security.AESAdapter;
66
import dev.matheuscruz.infra.security.BCryptAdapter;
7-
import dev.matheuscruz.presentation.data.Problem;
7+
import dev.matheuscruz.presentation.data.ProblemResponse;
88
import io.quarkus.logging.Log;
99
import io.quarkus.narayana.jta.QuarkusTransaction;
10+
import jakarta.annotation.security.PermitAll;
1011
import jakarta.validation.Valid;
1112
import jakarta.validation.constraints.Email;
1213
import jakarta.validation.constraints.NotBlank;
@@ -16,6 +17,7 @@
1617
import jakarta.ws.rs.core.Response;
1718

1819
@Path("/api/sign-up")
20+
@PermitAll
1921
public class SignUpResource {
2022

2123
UserRepository userRepository;
@@ -33,7 +35,7 @@ public Response signUp(@Valid SignUpRequest req) {
3335
boolean exists = this.userRepository.existsByEmail(req.email());
3436
if (exists) {
3537
return Response.status(Response.Status.CONFLICT)
36-
.entity(new Problem("Este nome de usuário já foi usado. Tente outro.")).build();
38+
.entity(new ProblemResponse("Este nome de usuário já foi usado. Tente outro.")).build();
3739
}
3840

3941
User user = User.create(req.email(), BCryptAdapter.encrypt(req.password()), req.firstName(), req.lastName(),

0 commit comments

Comments
 (0)