Skip to content

Commit 5fc2939

Browse files
finish alpha.1.1
1 parent b4f266f commit 5fc2939

63 files changed

Lines changed: 4288 additions & 2707 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.caesarjlee.caesarfinancialtracker.configurations;
2+
3+
import org.springframework.beans.factory.annotation.Value;
4+
import org.springframework.core.annotation.Order;
5+
import org.springframework.stereotype.Component;
6+
7+
import jakarta.servlet.*;
8+
import jakarta.servlet.http.*;
9+
import java.io.IOException;
10+
import java.util.*;
11+
12+
/*
13+
* block bots and unauthorized requests
14+
* only allow:
15+
* 1. allowed frontend origins (localhost and caesaris.net)
16+
* 2. with a valid Bearer token
17+
* 3. OPTIONS preflight requests
18+
*/
19+
@Component
20+
@Order(1)
21+
public class BotBlocker implements Filter {
22+
@Value("${app.cors.allowed-origins}") private String allowedOriginsRaw;
23+
24+
@Override
25+
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
26+
throws IOException, ServletException {
27+
HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
28+
HttpServletResponse httpServletResponse = (HttpServletResponse)servletResponse;
29+
30+
// allow CORS preflight
31+
if("OPTIONS".equalsIgnoreCase(httpServletRequest.getMethod())) {
32+
filterChain.doFilter(httpServletRequest, httpServletResponse);
33+
return;
34+
}
35+
36+
String origin = httpServletRequest.getHeader("Origin");
37+
String authorizationHeader = httpServletRequest.getHeader("Authorization");
38+
39+
List<String> allowedOrigins =
40+
Arrays.stream(allowedOriginsRaw.split(",")).map(String::trim).filter(str -> !str.isEmpty()).toList();
41+
42+
boolean hasValidOrigin = origin != null && allowedOrigins.contains(origin);
43+
boolean hasAuthoricationToken = authorizationHeader != null && authorizationHeader.startsWith("Bearer");
44+
45+
// block if neither an allowed origin nor a valid JWT token
46+
if(!hasValidOrigin && !hasAuthoricationToken) {
47+
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
48+
httpServletResponse.setContentType("application/json");
49+
httpServletResponse.getWriter().write("{\"error\": \"Access denied\"}");
50+
return;
51+
}
52+
53+
filterChain.doFilter(httpServletRequest, httpServletResponse);
54+
}
55+
}

backend/src/main/java/com/caesarjlee/caesarfinancialtracker/repositories/RecordRepository.java

Lines changed: 28 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,25 @@
22

33
import com.caesarjlee.caesarfinancialtracker.entities.RecordEntity;
44

5-
import java.math.BigDecimal;
6-
import java.time.LocalDate;
7-
import java.util.*;
8-
95
import org.springframework.data.domain.*;
106
import org.springframework.data.jpa.repository.*;
117
import org.springframework.data.repository.query.Param;
128

13-
public interface RecordRepository extends JpaRepository <RecordEntity, Long> {
9+
import java.math.BigDecimal;
10+
import java.time.LocalDate;
11+
import java.util.*;
12+
13+
public interface RecordRepository extends JpaRepository<RecordEntity, Long> {
1414
/*
1515
SELECT * FROM cft_records
1616
WHERE id = ? AND profile_id = ?
1717
*/
18-
Optional <RecordEntity> findByIdAndProfileId(Long id, Long profileId);
18+
Optional<RecordEntity> findByIdAndProfileId(Long id, Long profileId);
1919
/*
2020
SELECT * FROM cft_records
2121
WHERE profile_id = ?
2222
*/
23-
List <RecordEntity> findByProfileId(Long profileId);
23+
List<RecordEntity> findByProfileId(Long profileId);
2424
/*
2525
SELECT r.* FROM cft_records r
2626
JOIN cft_categories c ON c.id = r.category_id
@@ -35,8 +35,7 @@ public interface RecordRepository extends JpaRepository <RecordEntity, Long> {
3535
ORDER BY ? ASC/DESC
3636
LIMIT ? OFFSET ?
3737
*/
38-
@Query(
39-
"""
38+
@Query("""
4039
SELECT r FROM RecordEntity r
4140
WHERE r.profile.id = :profileId
4241
AND (:keyword IS NULL OR LOWER(r.name) LIKE LOWER(CONCAT('%', :keyword, '%')))
@@ -46,16 +45,11 @@ public interface RecordRepository extends JpaRepository <RecordEntity, Long> {
4645
AND (:dateEnd IS NULL OR r.date <= :dateEnd)
4746
AND (:priceLow IS NULL OR r.price >= :priceLow)
4847
AND (:priceHigh IS NULL OR r.price <= :priceHigh)
49-
"""
50-
)
51-
Page<RecordEntity> search(@Param("profileId") Long profileId,
52-
@Param("keyword") String keyword,
53-
@Param("type") String type,
54-
@Param("categoryId") Long categoryId,
55-
@Param("dateStart") LocalDate dateStart,
56-
@Param("dateEnd") LocalDate dateEnd,
57-
@Param("priceLow") BigDecimal priceLow,
58-
@Param("priceHigh") BigDecimal priceHigh,
48+
""")
49+
Page<RecordEntity> search(@Param("profileId") Long profileId, @Param("keyword") String keyword,
50+
@Param("type") String type, @Param("categoryId") Long categoryId,
51+
@Param("dateStart") LocalDate dateStart, @Param("dateEnd") LocalDate dateEnd,
52+
@Param("priceLow") BigDecimal priceLow, @Param("priceHigh") BigDecimal priceHigh,
5953
Pageable pageable);
6054
/*
6155
SELECT r.* FROM cft_records r
@@ -69,27 +63,19 @@ Page<RecordEntity> search(@Param("profileId") Long profileId,
6963
AND (? IS NULL OR r.category_id IN (?, ...))
7064
AND (? IS NULL OR LOWER(r.name) LIKE LOWER(CONCAT('%', ?, '%')))
7165
*/
72-
@Query(
73-
"""
74-
SELECT r FROM RecordEntity r
75-
WHERE r.profile.id = :profileId
76-
AND (:type IS NULL OR r.category.type = :type)
77-
AND (:dateStart IS NULL OR r.date >= :dateStart)
78-
AND (:dateEnd IS NULL OR r.date <= :dateEnd)
79-
AND (:priceLow IS NULL OR r.price >= :priceLow)
80-
AND (:priceHigh IS NULL OR r.price <= :priceHigh)
81-
AND (:#{#categories == null || #categories.isEmpty()} = true
82-
OR r.category.id IN :categories)
83-
AND (:keyword IS NULL OR LOWER(r.name) LIKE LOWER(CONCAT('%', :keyword, '%')))
84-
"""
85-
)
86-
List <RecordEntity> searchAll(@Param("profileId") Long profileId,
87-
@Param("type") String type,
88-
@Param("dateStart") LocalDate dateStart,
89-
@Param("dateEnd") LocalDate dateEnd,
90-
@Param("priceLow") BigDecimal priceLow,
91-
@Param("priceHigh") BigDecimal priceHigh,
92-
@Param("skipCategories") boolean skipCategories,
93-
@Param("categories") List <Long> categories,
94-
@Param("keyword") String keyword);
66+
@Query("""
67+
SELECT r FROM RecordEntity r
68+
WHERE r.profile.id = :profileId
69+
AND (:type IS NULL OR :type = 'all' OR LOWER(r.category.type) = LOWER(:type))
70+
AND (:dateStart IS NULL OR r.date >= :dateStart)
71+
AND (:dateEnd IS NULL OR r.date <= :dateEnd)
72+
AND (:priceLow IS NULL OR r.price >= :priceLow)
73+
AND (:priceHigh IS NULL OR r.price <= :priceHigh)
74+
AND (:categories IS NULL OR r.category.id IN :categories)
75+
AND (:keyword IS NULL OR LOWER(r.name) LIKE LOWER(CONCAT('%', :keyword, '%')))
76+
""")
77+
List<RecordEntity> searchAll(@Param("profileId") Long profileId, @Param("type") String type,
78+
@Param("dateStart") LocalDate dateStart, @Param("dateEnd") LocalDate dateEnd,
79+
@Param("priceLow") BigDecimal priceLow, @Param("priceHigh") BigDecimal priceHigh,
80+
@Param("categories") List<Long> categories, @Param("keyword") String keyword);
9581
}

backend/src/main/java/com/caesarjlee/caesarfinancialtracker/services/RecordService.java

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package com.caesarjlee.caesarfinancialtracker.services;
22

3-
import com.caesarjlee.caesarfinancialtracker.dtos.records.*;
43
import com.caesarjlee.caesarfinancialtracker.dtos.imports.ImportResponse;
4+
import com.caesarjlee.caesarfinancialtracker.dtos.records.*;
55
import com.caesarjlee.caesarfinancialtracker.entities.*;
66
import com.caesarjlee.caesarfinancialtracker.enumerations.*;
77
import com.caesarjlee.caesarfinancialtracker.exceptions.categories.CategoryNotFoundException;
8-
import com.caesarjlee.caesarfinancialtracker.exceptions.records.*;
98
import com.caesarjlee.caesarfinancialtracker.exceptions.pages.PageSizeException;
9+
import com.caesarjlee.caesarfinancialtracker.exceptions.records.*;
1010
import com.caesarjlee.caesarfinancialtracker.repositories.*;
1111
import com.caesarjlee.caesarfinancialtracker.utilities.*;
1212

@@ -18,7 +18,6 @@
1818
import java.time.LocalDate;
1919
import java.util.List;
2020
import java.util.stream.Collectors;
21-
2221
import lombok.RequiredArgsConstructor;
2322

2423
@Service
@@ -30,11 +29,11 @@ public class RecordService {
3029
private final ImportFiles importFiles;
3130
private final ExportFiles exportFiles;
3231

33-
//helpers
32+
// helpers
3433
private RecordResponse toResponse(RecordEntity entity) {
35-
return new RecordResponse(entity.getId(), entity.getName(), entity.getType(), entity.getIcon(), entity.getDate(),
36-
entity.getPrice(), entity.getDescription(), entity.getCreatedAt(),
37-
entity.getUpdatedAt(), entity.getCategory().getId());
34+
return new RecordResponse(entity.getId(), entity.getName(), entity.getType(), entity.getIcon(),
35+
entity.getDate(), entity.getPrice(), entity.getDescription(), entity.getCreatedAt(),
36+
entity.getUpdatedAt(), entity.getCategory().getId());
3837
}
3938

4039
private RecordOrders validOrder(String order) {
@@ -52,12 +51,12 @@ private Pageable validPage(String order, int page, int size) {
5251
}
5352

5453
private void validate(LocalDate start, LocalDate end, BigDecimal low, BigDecimal high) {
55-
//dates
54+
// dates
5655
if(end != null && end.isAfter(LocalDate.now()))
5756
throw new InvalidRecordDateException(end + " must be <= " + LocalDate.now());
5857
if(start != null && end != null && start.isAfter(end))
5958
throw new InvalidRecordDateException(start + " must be <= " + end);
60-
//prices
59+
// prices
6160
if(low != null && low.compareTo(BigDecimal.ZERO) < 0)
6261
throw new InvalidRecordPriceException(low + " must be >= 0");
6362
if(high != null && high.compareTo(BigDecimal.ZERO) < 0)
@@ -66,15 +65,15 @@ private void validate(LocalDate start, LocalDate end, BigDecimal low, BigDecimal
6665
throw new InvalidRecordPriceException(low + " must be <= " + high);
6766
}
6867

69-
private static String toKeyword(String keyword){
68+
private static String toKeyword(String keyword) {
7069
return keyword == null || keyword.isBlank() ? null : "%" + keyword.toLowerCase() + "%";
7170
}
7271

73-
private static String toType(String type){
72+
private static String toType(String type) {
7473
return type == null || type.isBlank() || "all".equalsIgnoreCase(type) ? null : type.toLowerCase();
7574
}
7675

77-
//crud
76+
// crud
7877
public RecordResponse create(RecordRequest request) {
7978
Long profileId = profileService.getCurrentProfile().getId();
8079
CategoryEntity category =
@@ -98,23 +97,26 @@ public Page<RecordResponse> read(String order, String type, String keyword, Long
9897
LocalDate dateEnd, BigDecimal priceLow, BigDecimal priceHigh, int page, int size) {
9998
validate(dateStart, dateEnd, priceLow, priceHigh);
10099
return recordRepository
101-
.search(profileService.getCurrentProfile().getId(), toKeyword(keyword), toType(type), categoryId, dateStart, dateEnd, priceLow,
102-
priceHigh, validPage(order, page, size))
100+
.search(profileService.getCurrentProfile().getId(), toKeyword(keyword), toType(type), categoryId, dateStart,
101+
dateEnd, priceLow, priceHigh, validPage(order, page, size))
103102
.map(this::toResponse);
104103
}
105104

106-
public List <RecordResponse> readAll(String type, LocalDate dateStart, LocalDate dateEnd, BigDecimal priceLow, BigDecimal priceHigh, List <Long> categories, String keyword){
105+
public List<RecordResponse> readAll(String type, LocalDate dateStart, LocalDate dateEnd, BigDecimal priceLow,
106+
BigDecimal priceHigh, List<Long> categories, String keyword) {
107107
validate(dateStart, dateEnd, priceLow, priceHigh);
108-
boolean skipCategories = categories == null || categories.isEmpty();
109-
return recordRepository.searchAll(profileService.getCurrentProfile().getId(), toType(type), dateStart, dateEnd, priceLow, priceHigh, skipCategories, skipCategories ? List.of(-1L) : categories, keyword)
108+
List<Long> categoryFilter = (categories == null || categories.isEmpty()) ? null : categories;
109+
return recordRepository
110+
.searchAll(profileService.getCurrentProfile().getId(), toType(type), dateStart, dateEnd, priceLow,
111+
priceHigh, categoryFilter, keyword)
110112
.stream()
111113
.map(this::toResponse)
112114
.collect(Collectors.toList());
113115
}
114116

115117
public RecordResponse update(Long id, RecordRequest request) {
116118
RecordEntity entity = recordRepository.findByIdAndProfileId(id, profileService.getCurrentProfile().getId())
117-
.orElseThrow(() -> new RecordNotFoundException(request.name()));
119+
.orElseThrow(() -> new RecordNotFoundException(request.name()));
118120
entity.setName(request.name());
119121
entity.setType(request.type() == null ? entity.getType() : request.type());
120122
entity.setIcon(request.icon() == null ? entity.getIcon() : request.icon());
@@ -125,13 +127,13 @@ public RecordResponse update(Long id, RecordRequest request) {
125127
request.categoryId() == null
126128
? entity.getCategory()
127129
: categoryRepository.findById(request.categoryId())
128-
.orElseThrow(() -> new CategoryNotFoundException(Long.toString(request.categoryId()))));
130+
.orElseThrow(() -> new CategoryNotFoundException(Long.toString(request.categoryId()))));
129131
return toResponse(recordRepository.save(entity));
130132
}
131133

132134
public void delete(Long id) {
133135
recordRepository.delete(recordRepository.findByIdAndProfileId(id, profileService.getCurrentProfile().getId())
134-
.orElseThrow(() -> new RecordNotFoundException(Long.toString(id))));
136+
.orElseThrow(() -> new RecordNotFoundException(Long.toString(id))));
135137
}
136138

137139
public ImportResponse importRecord(MultipartFile file) {

backend/src/main/java/com/caesarjlee/caesarfinancialtracker/utilities/JwtService.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212

1313
@Service
1414
public class JwtService {
15-
@Value("${jwt.secret}") private String SECRET;
16-
private static final long EXPIRATION = 1_000 * 60 * 60 * 24 * 3; // 3 days
15+
@Value("${jwt.secret}") private String SECRET;
16+
@Value("${jwt.expiration:86400000}") private long EXPIRATION;
1717

18-
private SecretKey getSigningKey() {
18+
private SecretKey getSigningKey() {
1919
return Keys.hmacShaKeyFor(SECRET.getBytes());
2020
}
2121

backend/src/main/resources/application.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
spring:
2+
# profiles:
3+
# active: local
24
jackson:
35
property-naming-strategy: SNAKE_CASE
46
datasource:
@@ -25,3 +27,4 @@ app:
2527

2628
jwt:
2729
secret: ${JWT_SECRET}
30+
expiration: ${JWT_EXPIRATION:86400000} #24 hours

frontend/index.html

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,36 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/png" href="src/assets/images/logo.png" />
56
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
8+
<!--SEO (Search Engine Optimization)-->
69
<title>Caesar Financial Tracker</title>
7-
<link rel="icon" href="src/assets/images/logo.png" type="image/png" />
10+
<meta
11+
name="description"
12+
content="Caesar Financial Tracker - Take over your wallet - track your income and expenses with
13+
ease."
14+
/>
15+
<meta
16+
name="keyword"
17+
content="financial tracker, expense tracker, income tracker, budget, money manager, java, spring
18+
boot, typescript, react"
19+
/>
20+
<meta name="author" content="Caesar James LEE" />
21+
22+
<!--tell Google to index home, /login and /signup pages only-->
23+
<meta name="robots" content="index, follow" />
24+
<meta name="googlebot" content="index, follow" />
25+
26+
<!--open graph to get better Google search results-->
27+
<meta property="og:type" content="website" />
28+
<meta property="og:title" content="Caesar Financial Tracker" />
29+
<meta property="og:description" content="Take over your wallet. Track your income and expenses with ease." />
30+
<meta property="og:url" content="https://caesaris.net" />
31+
<meta property="og:image" content="https://caesaris.net/src/assets/images/logo.png" />
32+
33+
<!--sitemap-->
34+
<link rel="sitemap" type="application/xml" href="/sitemap.xml" />
835
</head>
936
<body>
1037
<div id="root"></div>

0 commit comments

Comments
 (0)