wiki:otherdevelopment

Version 2 (modified by 233062, 3 days ago) ( diff )

--

Други Развојни Активности

Анализа на перформанси

Извештај за годишна финансиска активност на корисници

Анализата на перформанси се врши врз основа на моменталната состојба во базата, која има податоци кои служат за тестирање. Базата содржи: ~1000 корисници, ~2000 приходи (incomes), ~1000 finance профили.

SQL:

SELECT
    fb.user_id,
    fb.username,
    fb.email,
    COALESCE(ai.income_count, 0) AS income_count,
    COALESCE(ai.annual_total_income, 0) AS annual_total_income,
    COALESCE(ma.active_months, 0) AS active_months,
    ROUND((COALESCE(ma.active_months, 0) / 12.0)::numeric, 4) AS activity_ratio,
    ROUND((COALESCE(fb.spending_budget, 0) / 100.0) * COALESCE(ai.annual_total_income, 0), 2) AS spending_amount,
    ROUND((COALESCE(fb.saving_budget, 0) / 100.0) * COALESCE(ai.annual_total_income, 0), 2) AS saving_amount,
    ROUND((COALESCE(fb.investing_budget, 0) / 100.0) * COALESCE(ai.annual_total_income, 0), 2) AS investing_amount,
    ROUND((COALESCE(fb.donation_budget, 0) / 100.0) * COALESCE(ai.annual_total_income, 0), 2) AS donation_amount,
    ROUND((COALESCE(fb.credit, 0) / 100.0) * COALESCE(ai.annual_total_income, 0), 2) AS credit_amount
FROM finance_users fb
LEFT JOIN (
    SELECT user_id, COUNT(*) AS income_count, SUM(amount) AS annual_total_income
    FROM incomes
    WHERE EXTRACT(YEAR FROM date) = 2026
    GROUP BY user_id
) ai ON ai.user_id = fb.user_id
LEFT JOIN (
    SELECT user_id, COUNT(DISTINCT EXTRACT(MONTH FROM date)) AS active_months
    FROM incomes
    WHERE EXTRACT(YEAR FROM date) = 2026
    GROUP BY user_id
) ma ON ma.user_id = fb.user_id
ORDER BY annual_total_income DESC;

Индекси:

idx_incomes_user_date
CREATE INDEX idx_incomes_user_date ON incomes(user_id, date);

Подобрување: Index Scan наместо Seq Scan + подобро филтрирање по година

  1. idx_incomes_year
    CREATE INDEX idx_incomes_year ON incomes ((EXTRACT(YEAR FROM date)));
    

Подобрување: Избегнува full table scan за годишен филтер

3.

idx_incomes_covering
CREATE INDEX idx_incomes_covering ON incomes(user_id, date, amount);

Подобрување: Direct lookup

4.

CREATE UNIQUE INDEX users_pkey 
ON users(user_id);

Подобрување: Fast join

5.

CREATE INDEX idx_incomes_covering 
ON incomes(user_id, date, amount);

Подобрување: Index-only scan (без пристап до табелата)

==SQL за анализа на брзината

CREATE OR REPLACE FUNCTION test_finance_report()
RETURNS void AS $$
DECLARE
  start_time timestamptz;
  end_time timestamptz;
  duration int;
BEGIN
  start_time := clock_timestamp();

  PERFORM *
  FROM (
      SELECT
          fb.user_id,
          COALESCE(ai.income_count, 0),
          COALESCE(ai.annual_total_income, 0),
          COALESCE(ma.active_months, 0)
      FROM finance_users fb
      LEFT JOIN (
          SELECT user_id, COUNT(*), SUM(amount)
          FROM incomes
          WHERE EXTRACT(YEAR FROM date) = 2026
          GROUP BY user_id
      ) ai ON ai.user_id = fb.user_id
      LEFT JOIN (
          SELECT user_id, COUNT(DISTINCT EXTRACT(MONTH FROM date))
          FROM incomes
          WHERE EXTRACT(YEAR FROM date) = 2026
          GROUP BY user_id
      ) ma ON ma.user_id = fb.user_id
  ) t;

  end_time := clock_timestamp();
  duration := round(1000 * (extract(epoch FROM end_time) - extract(epoch FROM start_time)));

  RAISE NOTICE 'Query executed in: % ms', duration;
END;
$$ LANGUAGE plpgsql;

-- run 1: no indexes
SELECT test_finance_report();

-- run 2: add idx_incomes_user_date
CREATE INDEX idx_incomes_user_date ON incomes(user_id, date);
ANALYZE incomes;
SELECT test_finance_report();

-- run 3: add idx_incomes_year
CREATE INDEX idx_incomes_year ON incomes ((EXTRACT(YEAR FROM date)));
ANALYZE incomes;
SELECT test_finance_report();

-- run 4: add covering index
CREATE INDEX idx_incomes_covering ON incomes(user_id, date, amount);
ANALYZE incomes;
SELECT test_finance_report();

DROP FUNCTION test_finance_report();

Сумарно: Без индекси: ~61ms Со индекси: ~25ms Забрзување: ~2.4x

Безбедност и заштита

JWT Token Authorization (Spring Security)

JWT (JSON Web Token) e stateless начин на автентикација - server НЕ чува информации за активни сесии во база, туку сите потребни податоци се во самиот token кој корисникот го чува локално/cookie. JWT содржи енкодирана json структура на информации (user_id, email, role, expiry...).

Java код во Spring Boot:

@Configuration
@Getter @Setter
@ConfigurationProperties(prefix = "auth")
public class AuthProperties {
    private String secret;
    private int accessTokenMaxAge;
    private int refreshTokenMaxAge;
}


@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                .cors(Customizer.withDefaults())
                .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .httpBasic(AbstractHttpConfigurer::disable)
                .formLogin(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/auth/**").permitAll()
                        .anyRequest().authenticated());

        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

Хеширање на пасворди (BCrypt)

Пасвордите на корисниците се чуваат во база во хеширана форма преку BCrypt, а не како plain text. Ова овозможува сигурно чување на пасвордите.

public class LegacyPasswordEncoder implements PasswordEncoder {

    private final BCryptPasswordEncoder bcrypt = new BCryptPasswordEncoder();

    @Override
    public String encode(CharSequence rawPassword) {
        return bcrypt.encode(rawPassword);
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (encodedPassword == null || encodedPassword.isEmpty()) {
            return false;
        }
        if (encodedPassword.startsWith("$2a$") || encodedPassword.startsWith("$2b$")) {
            return bcrypt.matches(rawPassword, encodedPassword);
        }
        return rawPassword.toString().equals(encodedPassword);
    }
}

SQL Injection Prevention (Spring JPA/JPQL)

Целиот backend користи Spring Data JPA која автоматски генерира параметризирани пропити што спречуваат SQL injection.

Безбедно: Derived Query Methods

// UserRepository.java
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
    Optional<User> findByUsername(String username);
}


// SELECT * FROM users WHERE email = ?
// параметарот се праќа одвоено од SQL командата

Безбедно: @Query со @Param

// TaskRepository.java
@Modifying
@Query("update Task t set t.finished = false where t.disciplineUser.userId = :userId")
int resetFinishedForUser(@Param("userId") Long userId);

// Параметарот :userId се врзува безбедно преку JDBC, не преку string concatenation

Безбедно: Collection параметри

// TrainingSessionRepository.java
@java.util.List<TrainingSession> findByTrainingUser_UserIdAndDateIn(
    Long userId,
    Collection<LocalDate> dates
);


=== CORS Configuration ===

CORS е безбеден механизам за заштита од requests од различни домени. Со тоа се заштитуваме од можни злонамерни requests.

Java код во Spring Boot:
{{{
  @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of(
                "http://localhost:5173",
                "http://127.0.0.1:5173"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
        config.setAllowedHeaders(List.of("Authorization", "Content-Type", "Accept", "Origin", "X-Requested-With"));
        config.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }
}}}
Note: See TracWiki for help on using the wiki.