Skip to content

feat: implement user avatar and preferences management#19

Merged
zlovtnik merged 3 commits intomainfrom
dev
Nov 21, 2025
Merged

feat: implement user avatar and preferences management#19
zlovtnik merged 3 commits intomainfrom
dev

Conversation

@zlovtnik
Copy link
Copy Markdown
Owner

@zlovtnik zlovtnik commented Nov 21, 2025

Add comprehensive avatar upload/download/delete REST APIs and GraphQL mutations for user preferences, including caching with Redis. Includes troubleshooting docs for common issues like upload failures and cache invalidation. Enhances user settings with operations for API keys, password changes, and account status, driven by demand for personalized user experiences.

Summary by CodeRabbit

  • New Features

    • Complete User Settings UI and routes with avatar management (upload/download/delete), preferences, API key lifecycle, account status, and password operations exposed via REST and GraphQL.
  • Documentation

    • Expanded README/HELP with CORS, avatar limits, cache TTL, secrets guidance, Postman/SSL notes, GraphQL examples, routes, and troubleshooting/debugging.
  • Bug Fixes

    • Improved avatar upload error handling, fixed empty API key rawKey cases, ensured preferences cache invalidation, and handled account deactivation edge cases.

✏️ Tip: You can customize this high-level summary in your review settings.

Add comprehensive avatar upload/download/delete REST APIs and GraphQL mutations for user preferences, including caching with Redis. Includes troubleshooting docs for common issues like upload failures and cache invalidation. Enhances user settings with operations for API keys, password changes, and account status, driven by demand for personalized user experiences.
@vercel
Copy link
Copy Markdown

vercel bot commented Nov 21, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
spring-graphql Ready Ready Preview Comment Nov 21, 2025 8:00pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Nov 21, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds a complete user settings feature: DB migrations and new entities for avatars, preferences, API keys and deactivation audit; backend REST/GraphQL endpoints and reactive services (avatar storage, API-key lifecycle, preferences caching, ETag caching); frontend settings routes, components and service; CORS/config updates and documentation expansion.

Changes

Cohort / File(s) Summary
Documentation
HELP.md, README.md
Extensive operational docs, troubleshooting, GraphQL/REST examples for user settings (avatars, API keys, preferences, account status), config examples, secrets guidance, and Postman/SSL notes.
DB migrations
db/migration/V500__user_settings_profile.sql, db/migration/V501__api_keys_add_preview.sql
Add user_preferences, api_keys (with key_preview), account_deactivation_audit; extend users with avatar_key, account_status, account_deactivated_at; idempotent Oracle-aware DDL.
Frontend routes
frontend/src/app/app.routes.ts
Replace placeholder with lazy-loaded SettingsComponent and nested child routes: profile, preferences, api-keys, notifications plus default redirect.
Frontend components
frontend/src/app/features/settings/*
.../settings.component.ts, profile.component.ts, preferences.component.ts, api-keys.component.ts, notifications.component.ts
Add Settings layout and four standalone pages featuring avatar UI, preferences forms, API-keys UI (generate/revoke/delete), notifications settings, local state and lifecycle handling.
Frontend service
frontend/src/app/core/services/settings.service.ts
New SettingsService using Apollo/HttpClient exposing preferences, API-key lifecycle, account actions, password update, and REST avatar upload.
Backend controllers (REST & actuator)
src/main/java/com/rcs/ssf/controller/{UserProfileController, CacheMetricsController, ETagMetricsController}.java
New UserProfileController (avatar upload/download/delete) and controllers exposing cache/ETag metrics/status endpoints.
GraphQL schema & resolvers
src/main/resources/graphql/schema.graphqls, src/main/java/com/rcs/ssf/graphql/{SettingsMutation, AuthMutation}.java
GraphQL additions: AccountStatus, UserPreferences, ApiKey types/inputs, queries/mutations for preferences, API keys, account status, avatar upload stub, password; AuthMutation now truncates failure reasons.
Backend DTOs
src/main/java/com/rcs/ssf/dto/*
Added DTOs: AccountStatusDto, ApiKeyDto, AvatarUploadResponseDto, CreateApiKeyRequestDto, CreateApiKeyResponseDto, DeactivationReasonDto, UserPreferencesDto.
Backend entities
src/main/java/com/rcs/ssf/entity/*
Added AccountStatus enum, ApiKey, UserPreferences, AccountDeactivationAudit; extended User with avatarKey, accountStatus, accountDeactivatedAt.
Repositories
src/main/java/com/rcs/ssf/repository/*
New reactive repositories: ApiKeyRepository, UserPreferencesRepository, AccountDeactivationAuditRepository with query methods.
Backend services
src/main/java/com/rcs/ssf/service/*
New/updated services: ApiKeyService (generate/list/revoke/delete/validate), AccountService (deactivate/reactivate + audit), AvatarUploadService (MinIO upload/download/delete), UserPreferencesService (Redis caching + eviction), ETagCacheService (Redis-backed ETag caching and stats), AuditService.truncateFailureReason, UserService password update.
HTTP filter / config
src/main/java/com/rcs/ssf/http/ETagFilter.java, src/main/java/com/rcs/ssf/config/{CorsProperties, SecurityConfig}.java
ETagFilter now uses ETagCacheService; added CorsProperties bean and SecurityConfig updated to parse allowed origins from it.
Config files
src/main/resources/{application.yml, application-dev.yml, application-production.yml}
Added app.cors.allowed-origins, avatar and cache TTL defaults, management.metrics entries, logging adjustments, and production guidance for CORS env var.
Reactive transaction bean
src/main/java/com/rcs/ssf/service/reactive/ReactiveDataSourceConfiguration.java
Added R2DBC ReactiveTransactionManager bean wired to connection pool.
Utilities & constants
src/main/java/com/rcs/ssf/util/AccountConstants.java
Add account deactivation reason code constants.
Tests (small)
src/test/java/com/rcs/ssf/graphql/AdminMutationIntegrationTest.java
Minor test import adjustments (static asserts).

Sequence Diagram(s)

sequenceDiagram
    participant U as User (Browser)
    participant UI as Settings UI
    participant FSvc as Frontend SettingsService
    participant Apollo as Apollo Client
    participant Backend as Backend (GraphQL/REST)
    participant DB as Database
    participant Redis as Redis (preferences/ETag)
    participant MinIO as MinIO

    rect rgb(200,220,255)
      U->>UI: Update preferences
      UI->>FSvc: updateUserPreferences(prefs)
      FSvc->>Apollo: GraphQL mutation
      Apollo->>Backend: updateUserPreferences
      Backend->>DB: upsert user_preferences
      Backend->>Redis: evict user-preferences cache
      DB-->>Backend: OK
      Backend-->>Apollo: UserPreferencesDto
      Apollo-->>FSvc: response
      FSvc-->>UI: show success
    end

    rect rgb(220,240,220)
      U->>UI: Generate API key
      UI->>FSvc: generateApiKey(name, days)
      FSvc->>Apollo: GraphQL mutation
      Apollo->>Backend: generateApiKey
      Backend->>Backend: create rawKey/hash/preview
      Backend->>DB: insert api_keys
      DB-->>Backend: id
      Backend-->>Apollo: CreateApiKeyResponseDto (rawKey shown once)
      Apollo-->>FSvc: response
      FSvc-->>UI: display raw key modal
    end

    rect rgb(255,240,220)
      U->>UI: Upload avatar
      UI->>FSvc: uploadAvatar(file) (REST)
      FSvc->>Backend: POST /api/user/avatar
      Backend->>MinIO: upload object
      Backend->>DB: update users.avatar_key
      Backend->>Redis: invalidate ETag(s)
      MinIO-->>Backend: OK
      DB-->>Backend: OK
      Backend-->>FSvc: AvatarUploadResponseDto
      FSvc-->>UI: refresh avatar
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas to focus during review:

  • Reactive timeouts, error handling and ownership checks in ApiKeyService, AccountService, UserPreferencesService.
  • DB migrations (V500/V501) idempotency and Oracle-specific EXCEPTION handling.
  • ETagCacheService correctness, Redis failure fallbacks, and ETagFilter integration.
  • AvatarUploadService (MinIO) — validation, streaming, error mapping and transactional updates to user record.
  • GraphQL schema ↔ DTO/entity mapping, null-safety and security of sensitive fields (rawKey handling).
  • CORS parsing changes in SecurityConfig and CorsProperties usage.

Possibly related PRs

Poem

🐇 I hopped through code with nimble paws,

Avatars, keys, and prefs—no pause.
Cache and GraphQL, routes that sing,
Upload, revoke, and toggles—spring!
A little rabbit cheers this patch with claws.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main feature: implementing user avatar and preferences management, which aligns with the primary changes adding avatar upload/download/delete REST APIs, GraphQL mutations for preferences, API keys, password changes, and account status operations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5635b58 and 1c5116a.

📒 Files selected for processing (12)
  • frontend/src/app/features/settings/pages/notifications.component.ts (1 hunks)
  • src/main/java/com/rcs/ssf/controller/CacheMetricsController.java (1 hunks)
  • src/main/java/com/rcs/ssf/controller/UserProfileController.java (1 hunks)
  • src/main/java/com/rcs/ssf/dto/ApiKeyDto.java (1 hunks)
  • src/main/java/com/rcs/ssf/graphql/AccountStatusResponse.java (1 hunks)
  • src/main/java/com/rcs/ssf/graphql/SettingsMutation.java (1 hunks)
  • src/main/java/com/rcs/ssf/service/ApiKeyService.java (1 hunks)
  • src/main/java/com/rcs/ssf/service/UserPreferencesService.java (1 hunks)
  • src/main/java/com/rcs/ssf/service/UserService.java (3 hunks)
  • src/main/java/com/rcs/ssf/service/reactive/ReactiveDataSourceConfiguration.java (2 hunks)
  • src/main/resources/graphql/schema.graphqls (4 hunks)
  • src/test/java/com/rcs/ssf/graphql/AdminMutationIntegrationTest.java (1 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (26)
src/main/java/com/rcs/ssf/controller/CacheMetricsController.java (4)

13-27: Controller wiring and actuator-style base path

The controller wiring and constructor injection look clean and consistent with the existing ETagMetricsController pattern. One thing to double‑check is that your security config treats /actuator/cache-metrics/** the way you expect (these are regular MVC endpoints, not Spring Boot Actuator @Endpoint beans), especially if you rely on actuator‑specific exposure/authorization rules for other management URLs.


29-63: Caffeine stats collection is sound; minor micro‑refactor possible

Logic for iterating caches, guarding null, and only handling Caffeine‑backed caches is solid. You currently call caffeineCache.stats() twice; you can reuse the same stats instance both for the stringified "stats" field and the individual counters:

-                    Map<String, Object> cacheStats = new HashMap<>();
-                    cacheStats.put("stats", caffeineCache.stats().toString());
-                    cacheStats.put("estimatedSize", caffeineCache.estimatedSize());
-                    
-                    // Parse stats into individual metrics
-                    var stats = caffeineCache.stats();
+                    var stats = caffeineCache.stats();
+
+                    Map<String, Object> cacheStats = new HashMap<>();
+                    cacheStats.put("stats", stats.toString());
+                    cacheStats.put("estimatedSize", caffeineCache.estimatedSize());
+                    
+                    // Parse stats into individual metrics

This keeps behavior identical while avoiding the duplicate call and slightly tightening the code.


68-124: Reduce duplication in Micrometer metric extraction and verify metric names/types

The three blocks for cache.hits, cache.misses, and cache.puts are structurally identical. Consider extracting a small helper (or local function) that, given a metric name, returns the List<Map<String, Object>> for that name. That will make it easier to add more cache metrics later and avoid copy‑paste divergence.

Also, please double‑check against your Micrometer/Spring configuration that:

  • cache.hits, cache.misses, and cache.puts are indeed the metric names being emitted, and
  • they are exposed as gauges (not counters/timers), since you are specifically calling .gauges() here.

Some Micrometer cache instrumentation setups instead emit counters with names like cache.gets and use tags (e.g., result=hit|miss), so a quick verification against the running app’s /actuator/metrics output is worthwhile.


129-171: Summary aggregation is correct; consider reusing logic and clarifying hit‑rate semantics

The aggregation over all Caffeine caches and the per‑cache summary map look correct. Two small improvement ideas:

  1. Reuse iteration logic: getSummary() and getCaffeineStats() both walk cacheManager.getCacheNames() and filter on Caffeine caches. Pulling this into a shared private helper (e.g., to yield a typed view of cacheName + CacheStats) would reduce duplication and future drift.

  2. Overall hit‑rate representation: Right now overallHitRate is:

    • "N/A (no misses)" when totalMisses == 0, even if there have been many hits, and
    • a "%.2f%%" string otherwise.

    If consumers expect a numeric value, you might prefer always returning a percentage (and maybe a separate "note" field), or distinguishing the “no data at all” case (totalHits + totalMisses == 0) from the “100% hit rate” case (totalMisses == 0 && totalHits > 0).

These are mostly semantics/maintainability tweaks; current behavior is functionally fine.

db/migration/V501__api_keys_add_preview.sql (1)

1-5: Consider VARCHAR2(20) size constraint for future flexibility.

The 20-character limit for key_preview may be restrictive if preview formats evolve (e.g., longer prefixes, different masking patterns). Consider whether a slightly larger size like VARCHAR2(50) would provide better future-proofing without significant storage overhead.

src/main/java/com/rcs/ssf/dto/CreateApiKeyRequestDto.java (1)

19-29: Consider reviewing the 10-year maximum expiry for security.

The MAX_EXPIRY_DAYS = 3650 (approximately 10 years) is quite long for API keys from a security perspective. Industry best practices often recommend shorter maximum lifetimes (e.g., 1-2 years) to limit exposure from compromised keys and encourage regular rotation.

Consider reducing the maximum to a more security-conscious value:

     private static final long DEFAULT_EXPIRY_DAYS = 90;
     private static final long MIN_EXPIRY_DAYS = 1;
-    private static final long MAX_EXPIRY_DAYS = 3650; // ~10 years
+    private static final long MAX_EXPIRY_DAYS = 730; // ~2 years

     @NotBlank(message = "Key name must be provided")
     @Size(min = 1, max = 100, message = "Key name must be between 1 and 100 characters")
     private String keyName;

     @Min(value = MIN_EXPIRY_DAYS, message = "Expiration days must be at least 1")
-    @Max(value = MAX_EXPIRY_DAYS, message = "Expiration days cannot exceed 3650 days (~10 years)")
+    @Max(value = MAX_EXPIRY_DAYS, message = "Expiration days cannot exceed 730 days (~2 years)")
     private Long expiresInDays; // Optional, defaults to 90 days if null
frontend/src/app/features/settings/settings.component.ts (1)

93-98: Consider more robust URL matching for redirect logic.

The exact string comparison this.router.url === '/settings' won't handle trailing slashes (e.g., /settings/) or query parameters (e.g., /settings?tab=profile). Consider using a more flexible approach:

   ngOnInit(): void {
     // Navigate to profile by default
-    if (this.router.url === '/settings') {
+    if (this.router.url === '/settings' || this.router.url === '/settings/') {
       this.router.navigate(['/settings/profile']);
     }
   }

Or use route matching:

ngOnInit(): void {
  // Navigate to profile by default if on root settings path
  const urlTree = this.router.parseUrl(this.router.url);
  if (urlTree.root.children['primary']?.segments.length === 1 && 
      urlTree.root.children['primary']?.segments[0].path === 'settings') {
    this.router.navigate(['/settings/profile']);
  }
}
src/main/java/com/rcs/ssf/controller/ETagMetricsController.java (1)

23-36: Consider securing actuator endpoints and using a structured response DTO.

Two suggestions for improvement:

  1. Security: Actuator endpoints typically expose operational data and should be secured in production. Consider adding @PreAuthorize or configuring endpoint security in Spring Boot actuator properties.

  2. Type safety: Using Map<String, Object> for the response loses type safety. Consider creating a dedicated response DTO (e.g., ETagCacheStatusResponse) for better API documentation and client code generation.

Example DTO approach:

public record ETagCacheStatusResponse(
    boolean operational,
    int ttlMinutes,
    String status,
    String description
) {}

@GetMapping("/status")
public ETagCacheStatusResponse getETagCacheStatus() {
    var stats = etagCacheService.getStats();
    return new ETagCacheStatusResponse(
        stats.operational(),
        stats.ttlMinutes(),
        stats.status(),
        "ETags are cached in Redis with " + stats.ttlMinutes() + 
                " minute TTL to avoid repeated SHA-256 computation"
    );
}
src/main/java/com/rcs/ssf/repository/UserPreferencesRepository.java (1)

7-13: Repository signature looks good; consider tweaking the caching comment

UserPreferencesRepository correctly extends R2dbcRepository<UserPreferences, Long> and findByUserId is idiomatic for Spring Data R2DBC. If Redis caching is actually handled in the service layer, you may want to rephrase the Javadoc to avoid implying this interface itself “supports Redis caching” rather than being cacheable by higher layers.

src/main/java/com/rcs/ssf/dto/AccountStatusDto.java (1)

14-21: Prefer using the AccountStatus enum instead of raw String for status

Given there is an AccountStatus enum (ACTIVE, DEACTIVATED, SUSPENDED), modeling status as a String here loses type safety and risks drift between allowed values and actual enum constants. Consider:

  • Changing the field to private AccountStatus status;
  • Replacing @NotBlank with @NotNull (and, if needed, adding an enum‑validation constraint)

to keep the DTO aligned with the rest of the account status model and GraphQL/schema expectations.

src/main/java/com/rcs/ssf/dto/DeactivationReasonDto.java (1)

8-20: Structured reason input looks good; consider constraining reasonCode

The DTO is well-structured for deactivation input. To avoid arbitrary codes creeping in over time, you might later introduce an enum (or at least central constants) for reasonCode (e.g., USER_REQUESTED, INACTIVITY, etc.) and map them here, rather than taking a free‑form String.

src/main/java/com/rcs/ssf/dto/AvatarUploadResponseDto.java (1)

3-51: Avatar upload response DTO is fine; optional Lombok alignment

The DTO is straightforward and correct. For consistency with the rest of the dto package (which mostly uses Lombok), you could replace the manual boilerplate with:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class AvatarUploadResponseDto {
    private String avatarKey;
    private String avatarUrl;
    private String message;
}

But the current explicit form is also perfectly valid.

frontend/src/app/features/settings/pages/profile.component.ts (1)

168-176: Avatar state is never updated, so “Remove Avatar” is always disabled

hasAvatar is initialized to false and only updated in the commented‑out block inside the getCurrentUser$ subscription. As a result:

  • [disabled]="!hasAvatar" keeps the “Remove Avatar” button permanently disabled.
  • handleAvatarChange doesn’t update avatarUrl / hasAvatar either.

Once the backend/avatar field is available on User, re‑enable the logic that derives avatarUrl and hasAvatar from user.avatarKey (or equivalent), and/or update them from the upload response so the remove button reflects actual avatar state.

Also applies to: 219-238

src/main/java/com/rcs/ssf/http/ETagFilter.java (1)

45-52: Delegating ETag computation to ETagCacheService is clean and maintains behavior

Constructor injection of ETagCacheService and using getOrComputeETag(responseBody) keeps the previous flow intact while enabling centralized caching. As long as the service guarantees a non-null ETag string for any byte array, this filter remains safe and side‑effect compatible with the old implementation.

Also applies to: 68-70

frontend/src/app/app.routes.ts (1)

53-95: Settings child routes wiring is correct; consider trimming duplicate redirects

The nested children under path: 'settings' with a default '' -> 'profile' redirect and the lazy SettingsComponent align well with Angular’s standalone routing. Given SettingsComponent already redirects /settings to /settings/profile on init, you could remove either that logic or this child redirect to avoid redundant behavior, but it’s not functionally harmful.

src/main/java/com/rcs/ssf/service/UserPreferencesService.java (2)

30-58: Consider concurrency on first-time creation and align with cache/TTL config

On a cache miss, two concurrent calls to getPreferencesByUserId for the same userId can both see an empty result and attempt save(new UserPreferences(userId)). If there is a uniqueness constraint on user_id, one insert will fail; if not, you may end up with duplicate rows.

If that matters in your deployment, consider:

  • Enforcing uniqueness on user_id and handling duplicate-key errors by re-reading, or
  • Using an idempotent “get-or-create” pattern at the database level (e.g., upsert / MERGE).

Also, the Javadoc mentions a 60‑minute cache duration, but that’s governed by the cache configuration rather than this class. If the TTL may change, it might be safer to either reference configuration (app.cache.user-preferences.ttl) or remove the hardcoded duration from the comment.


63-99: Update updatedAt when mutating user preferences

updatePreferences selectively mutates fields and persists them, but it never advances updatedAt, even though the entity exposes that column and the create‑time constructor initializes it. If any part of the system (UI or auditing) relies on updatedAt, it will silently become stale.

You could set it just before saving:

-        if (dto.getNotificationSecurityUpdates() != null) {
-            existing.setNotificationSecurityUpdates(dto.getNotificationSecurityUpdates());
-        }
-        UserPreferences updated = userPreferencesRepository
+        if (dto.getNotificationSecurityUpdates() != null) {
+            existing.setNotificationSecurityUpdates(dto.getNotificationSecurityUpdates());
+        }
+
+        existing.setUpdatedAt(System.currentTimeMillis());
+
+        UserPreferences updated = userPreferencesRepository
                 .save(existing)
                 .timeout(OPERATION_TIMEOUT)
                 .block();

This keeps the timestamp semantics consistent with the creation path.

src/main/java/com/rcs/ssf/service/AvatarUploadService.java (1)

82-101: Make "avatar not found" handling more robust than message string matching

Catch ErrorResponseException (a subclass of MinioException) specifically and check the structured error code rather than relying on string matching. Current approach catches the parent MinioException and uses e.getMessage().contains("The specified key does not exist"), which is brittle—error messages can change, and other failure modes may produce similar text.

Recommended pattern for MinIO 8.6.0:

  • Import and catch ErrorResponseException, then call e.errorResponse() to access the error code
  • Check the error code for "NoSuchKey" or check the HTTP status code for 404
  • This makes the code resilient to message text changes and explicitly distinguishes not-found from other failures

Suggested refactor in AvatarUploadService.downloadAvatar() (lines 82–101):

catch (ErrorResponseException e) {
    if ("NoSuchKey".equals(e.errorResponse().code()) || e.response().code() == 404) {
        log.warn("Avatar not found: {}", objectKey);
        return Optional.empty();
    }
    throw e;
} catch (MinioException e) {
    throw e;
}

You will also need to add the import:

import io.minio.errors.ErrorResponseException;

Additionally, document that callers must close the returned InputStream to release network resources (already shown in MinIO's getObject javadoc as best practice).

src/main/java/com/rcs/ssf/controller/UserProfileController.java (3)

82-85: Improve error response to include error details.

The error handling swallows all exception details and returns an empty 500 response. Consider including an error message or using a structured error response DTO to help clients diagnose upload failures.

Apply this diff to improve error handling:

-        .onErrorResume(e -> Mono.just(
-            ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()
-        ));
+        .onErrorResume(e -> {
+          String errorMsg = e.getMessage() != null ? e.getMessage() : "Avatar upload failed";
+          return Mono.just(
+              ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+                  .body(new AvatarUploadResponseDto(null, null, errorMsg))
+          );
+        });

109-112: Consider memory implications of loading entire avatar in memory.

Using readAllBytes() loads the entire avatar file into memory. For large images (up to 5MB as mentioned in comments), this could cause memory pressure under high concurrency.

Consider streaming the response body instead:

return Mono.fromCallable(() -> avatarUploadService.downloadAvatar(avatarKey))
    .subscribeOn(Schedulers.boundedElastic())
    .map(avatarDataOpt -> {
      if (avatarDataOpt.isEmpty()) {
        return (ResponseEntity<?>) ResponseEntity.notFound().build();
      }
      
      String contentType = determineContentType(avatarKey);
      
      return (ResponseEntity<?>) ResponseEntity.ok()
          .header("Content-Type", contentType)
          .header("Cache-Control", "public, max-age=86400")
          .body(new InputStreamResource(avatarDataOpt.get()));
    })
    // ... rest of error handling

Note: You'll need to import org.springframework.core.io.InputStreamResource and adjust the return type.


178-184: Fragile exception handling relying on message content.

Parsing exception messages with string matching (line 180) is brittle and can break if error messages change. Consider creating custom exceptions or checking exception types instead.

If AvatarUploadService can throw custom exceptions, use type-based handling:

         .onErrorResume(e -> {
-          // Map exceptions to appropriate HTTP status codes
-          if (e.getMessage() != null && e.getMessage().contains("not found")) {
+          if (e instanceof AvatarNotFoundException) {
             return Mono.just((ResponseEntity<?>) ResponseEntity.status(HttpStatus.NOT_FOUND).build());
           }
           return Mono.just((ResponseEntity<?>) ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build());
         })
frontend/src/app/features/settings/pages/api-keys.component.ts (1)

235-260: Complete the GraphQL integration for key generation.

The submitGenerateKey method has the GraphQL mutation commented out, leaving a non-functional placeholder. The form and UI are complete but won't actually generate keys.

Do you want me to help complete the GraphQL integration? I can provide the correct service calls based on the SettingsService interface.

frontend/src/app/core/services/settings.service.ts (1)

290-309: Complete avatar upload implementation or document it as future work.

The uploadAvatar method is a placeholder that immediately throws an error. Since the PR introduces UserProfileController with a REST endpoint for avatar upload (POST /api/user/avatar), this method should use HttpClient to call that endpoint.

Do you want me to generate the implementation using Angular's HttpClient?

Example:

uploadAvatar(file: File): Observable<any> {
  const formData = new FormData();
  formData.append('file', file);
  
  return this.http.post<any>('/api/user/avatar', formData).pipe(
    map(response => ({
      success: true,
      avatarKey: response.avatarKey,
      avatarUrl: response.avatarUrl,
      message: response.message
    }))
  );
}

Note: You'll need to inject HttpClient from @angular/common/http.

src/main/java/com/rcs/ssf/graphql/SettingsMutation.java (2)

143-149: Avoid swallowing all delete errors behind a Boolean flag

deleteApiKey maps every error (not found, unauthorized, repository failures, timeouts, etc.) to false via .onErrorReturn(false). This makes it hard for clients and operators to distinguish user errors from server-side issues and hides useful error messages that ApiKeyService.deleteApiKey already provides.

Consider either:

  • letting exceptions propagate so GraphQL returns structured errors, or
  • mapping only specific expected exceptions (e.g. IllegalArgumentException) to false while letting unexpected exceptions bubble up.

74-80: Update Javadoc to reflect fully reactive getAccountStatus

The comment still says “blocks for GraphQL response”, but the method now returns Mono<String> and delegates to a reactive service without explicit blocking. It would be clearer to update the Javadoc to describe it as a reactive resolver returning a Mono.

src/main/java/com/rcs/ssf/service/ApiKeyService.java (1)

184-256: Be mindful of O(n) validation cost as active key count grows

validateApiKeyReactive (and the deprecated validateApiKey) iterate over all active keys via findAllActive(currentTime) and passwordEncoder.matches, which is O(n) in the number of active keys. The Javadoc already calls this out and suggests storing a hash prefix.

As active-key volume increases, consider:

  • adding an indexed “hash prefix” or similar lookup hint so validation can short-list candidates, and/or
  • enforcing reasonable limits on the number of active keys per user to keep findAllActive bounded.

This is mostly an operational/scalability concern; the current implementation is correct for moderate volumes.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c961c21 and 4d42a41.

📒 Files selected for processing (46)
  • HELP.md (1 hunks)
  • README.md (4 hunks)
  • db/migration/V500__user_settings_profile.sql (1 hunks)
  • db/migration/V501__api_keys_add_preview.sql (1 hunks)
  • frontend/src/app/app.routes.ts (1 hunks)
  • frontend/src/app/core/services/settings.service.ts (1 hunks)
  • frontend/src/app/features/settings/pages/api-keys.component.ts (1 hunks)
  • frontend/src/app/features/settings/pages/notifications.component.ts (1 hunks)
  • frontend/src/app/features/settings/pages/preferences.component.ts (1 hunks)
  • frontend/src/app/features/settings/pages/profile.component.ts (1 hunks)
  • frontend/src/app/features/settings/settings.component.ts (1 hunks)
  • src/main/java/com/rcs/ssf/config/CorsProperties.java (1 hunks)
  • src/main/java/com/rcs/ssf/config/SecurityConfig.java (2 hunks)
  • src/main/java/com/rcs/ssf/controller/AuthController.java (1 hunks)
  • src/main/java/com/rcs/ssf/controller/CacheMetricsController.java (1 hunks)
  • src/main/java/com/rcs/ssf/controller/ETagMetricsController.java (1 hunks)
  • src/main/java/com/rcs/ssf/controller/UserProfileController.java (1 hunks)
  • src/main/java/com/rcs/ssf/dto/AccountStatusDto.java (1 hunks)
  • src/main/java/com/rcs/ssf/dto/ApiKeyDto.java (1 hunks)
  • src/main/java/com/rcs/ssf/dto/AvatarUploadResponseDto.java (1 hunks)
  • src/main/java/com/rcs/ssf/dto/CreateApiKeyRequestDto.java (1 hunks)
  • src/main/java/com/rcs/ssf/dto/CreateApiKeyResponseDto.java (1 hunks)
  • src/main/java/com/rcs/ssf/dto/DeactivationReasonDto.java (1 hunks)
  • src/main/java/com/rcs/ssf/dto/UserPreferencesDto.java (1 hunks)
  • src/main/java/com/rcs/ssf/entity/AccountDeactivationAudit.java (1 hunks)
  • src/main/java/com/rcs/ssf/entity/AccountStatus.java (1 hunks)
  • src/main/java/com/rcs/ssf/entity/ApiKey.java (1 hunks)
  • src/main/java/com/rcs/ssf/entity/User.java (2 hunks)
  • src/main/java/com/rcs/ssf/entity/UserPreferences.java (1 hunks)
  • src/main/java/com/rcs/ssf/graphql/AuthMutation.java (1 hunks)
  • src/main/java/com/rcs/ssf/graphql/SettingsMutation.java (1 hunks)
  • src/main/java/com/rcs/ssf/http/ETagFilter.java (4 hunks)
  • src/main/java/com/rcs/ssf/repository/AccountDeactivationAuditRepository.java (1 hunks)
  • src/main/java/com/rcs/ssf/repository/ApiKeyRepository.java (1 hunks)
  • src/main/java/com/rcs/ssf/repository/UserPreferencesRepository.java (1 hunks)
  • src/main/java/com/rcs/ssf/service/AccountService.java (1 hunks)
  • src/main/java/com/rcs/ssf/service/ApiKeyService.java (1 hunks)
  • src/main/java/com/rcs/ssf/service/AuditService.java (1 hunks)
  • src/main/java/com/rcs/ssf/service/AvatarUploadService.java (1 hunks)
  • src/main/java/com/rcs/ssf/service/ETagCacheService.java (1 hunks)
  • src/main/java/com/rcs/ssf/service/UserPreferencesService.java (1 hunks)
  • src/main/java/com/rcs/ssf/service/UserService.java (2 hunks)
  • src/main/resources/application-dev.yml (1 hunks)
  • src/main/resources/application-production.yml (1 hunks)
  • src/main/resources/application.yml (3 hunks)
  • src/main/resources/graphql/schema.graphqls (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (26)
src/main/java/com/rcs/ssf/controller/ETagMetricsController.java (1)
src/main/java/com/rcs/ssf/controller/CacheMetricsController.java (1)
  • RestController (17-172)
frontend/src/app/app.routes.ts (1)
frontend/src/app/core/guards/auth.guard.ts (1)
  • authGuard (21-40)
frontend/src/app/features/settings/pages/preferences.component.ts (2)
frontend/src/app/features/settings/pages/notifications.component.ts (1)
  • Component (20-347)
frontend/src/app/features/settings/settings.component.ts (1)
  • Component (16-99)
src/main/java/com/rcs/ssf/dto/CreateApiKeyResponseDto.java (2)
src/main/java/com/rcs/ssf/dto/ApiKeyDto.java (1)
  • Data (12-29)
src/main/java/com/rcs/ssf/dto/CreateApiKeyRequestDto.java (1)
  • Data (15-41)
src/main/java/com/rcs/ssf/dto/AccountStatusDto.java (4)
src/main/java/com/rcs/ssf/dto/ApiKeyDto.java (1)
  • Data (12-29)
src/main/java/com/rcs/ssf/dto/CreateApiKeyRequestDto.java (1)
  • Data (15-41)
src/main/java/com/rcs/ssf/dto/DeactivationReasonDto.java (1)
  • Data (12-21)
src/main/java/com/rcs/ssf/dto/UserPreferencesDto.java (1)
  • Data (12-32)
src/main/java/com/rcs/ssf/repository/ApiKeyRepository.java (1)
src/main/java/com/rcs/ssf/service/reactive/ReactiveDataSourceConfiguration.java (1)
  • org (343-460)
src/main/java/com/rcs/ssf/repository/AccountDeactivationAuditRepository.java (1)
src/main/java/com/rcs/ssf/service/reactive/ReactiveDataSourceConfiguration.java (1)
  • org (343-460)
frontend/src/app/features/settings/pages/profile.component.ts (3)
frontend/src/app/features/settings/pages/api-keys.component.ts (1)
  • Component (35-336)
frontend/src/app/features/settings/settings.component.ts (1)
  • Component (16-99)
frontend/src/app/core/services/auth.service.ts (1)
  • User (26-30)
src/main/java/com/rcs/ssf/dto/CreateApiKeyRequestDto.java (2)
src/main/java/com/rcs/ssf/dto/ApiKeyDto.java (1)
  • Data (12-29)
src/main/java/com/rcs/ssf/dto/CreateApiKeyResponseDto.java (1)
  • Data (12-24)
frontend/src/app/features/settings/pages/notifications.component.ts (3)
frontend/src/app/features/settings/pages/api-keys.component.ts (1)
  • Component (35-336)
frontend/src/app/features/settings/pages/preferences.component.ts (1)
  • Component (20-265)
frontend/src/app/features/settings/settings.component.ts (1)
  • Component (16-99)
frontend/src/app/features/settings/pages/api-keys.component.ts (2)
frontend/src/app/features/settings/pages/notifications.component.ts (1)
  • Component (20-347)
frontend/src/app/features/settings/settings.component.ts (1)
  • Component (16-99)
src/main/java/com/rcs/ssf/entity/UserPreferences.java (3)
src/main/java/com/rcs/ssf/dto/UserPreferencesDto.java (1)
  • Data (12-32)
src/main/java/com/rcs/ssf/entity/ApiKey.java (1)
  • Table (14-87)
src/main/java/com/rcs/ssf/entity/User.java (1)
  • Table (14-108)
frontend/src/app/features/settings/settings.component.ts (4)
frontend/src/app/features/settings/pages/api-keys.component.ts (1)
  • Component (35-336)
frontend/src/app/features/settings/pages/notifications.component.ts (1)
  • Component (20-347)
frontend/src/app/features/settings/pages/preferences.component.ts (1)
  • Component (20-265)
frontend/src/app/features/settings/pages/profile.component.ts (1)
  • Component (22-272)
src/main/java/com/rcs/ssf/repository/UserPreferencesRepository.java (1)
src/main/java/com/rcs/ssf/service/reactive/ReactiveDataSourceConfiguration.java (1)
  • org (343-460)
src/main/java/com/rcs/ssf/service/AccountService.java (2)
src/main/java/com/rcs/ssf/graphql/SettingsMutation.java (1)
  • Slf4j (31-249)
frontend/src/app/core/services/settings.service.ts (3)
  • deactivateAccount (253-259)
  • reactivateAccount (264-270)
  • getAccountStatus (242-248)
src/main/java/com/rcs/ssf/dto/UserPreferencesDto.java (5)
src/main/java/com/rcs/ssf/dto/AccountStatusDto.java (1)
  • Data (11-22)
src/main/java/com/rcs/ssf/dto/ApiKeyDto.java (1)
  • Data (12-29)
src/main/java/com/rcs/ssf/dto/CreateApiKeyRequestDto.java (1)
  • Data (15-41)
src/main/java/com/rcs/ssf/dto/CreateApiKeyResponseDto.java (1)
  • Data (12-24)
src/main/java/com/rcs/ssf/dto/DeactivationReasonDto.java (1)
  • Data (12-21)
src/main/java/com/rcs/ssf/controller/UserProfileController.java (1)
src/main/java/com/rcs/ssf/dto/AvatarUploadResponseDto.java (1)
  • AvatarUploadResponseDto (6-51)
src/main/java/com/rcs/ssf/service/AvatarUploadService.java (1)
frontend/src/app/core/services/settings.service.ts (1)
  • uploadAvatar (290-309)
src/main/java/com/rcs/ssf/service/UserService.java (1)
frontend/src/app/core/services/settings.service.ts (1)
  • updatePassword (277-284)
src/main/java/com/rcs/ssf/entity/AccountDeactivationAudit.java (4)
src/main/java/com/rcs/ssf/dto/DeactivationReasonDto.java (1)
  • Data (12-21)
src/main/java/com/rcs/ssf/entity/ApiKey.java (1)
  • Table (14-87)
src/main/java/com/rcs/ssf/entity/User.java (1)
  • Table (14-108)
src/main/java/com/rcs/ssf/entity/UserPreferences.java (1)
  • Table (14-76)
src/main/java/com/rcs/ssf/controller/CacheMetricsController.java (1)
src/main/java/com/rcs/ssf/controller/ETagMetricsController.java (1)
  • RestController (13-37)
src/main/java/com/rcs/ssf/graphql/SettingsMutation.java (5)
src/main/java/com/rcs/ssf/security/AuthenticatedUser.java (1)
  • AuthenticatedUser (9-24)
src/main/java/com/rcs/ssf/service/AccountService.java (1)
  • Slf4j (20-154)
src/main/java/com/rcs/ssf/service/ApiKeyService.java (1)
  • Slf4j (24-327)
src/main/java/com/rcs/ssf/service/UserPreferencesService.java (1)
  • Slf4j (18-126)
src/main/java/com/rcs/ssf/service/UserService.java (1)
  • Slf4j (36-445)
src/main/java/com/rcs/ssf/entity/ApiKey.java (5)
src/main/java/com/rcs/ssf/dto/ApiKeyDto.java (1)
  • Data (12-29)
src/main/java/com/rcs/ssf/dto/CreateApiKeyRequestDto.java (1)
  • Data (15-41)
src/main/java/com/rcs/ssf/dto/CreateApiKeyResponseDto.java (1)
  • Data (12-24)
src/main/java/com/rcs/ssf/entity/User.java (1)
  • Table (14-108)
src/main/java/com/rcs/ssf/entity/UserPreferences.java (1)
  • Table (14-76)
src/main/java/com/rcs/ssf/dto/DeactivationReasonDto.java (1)
src/main/java/com/rcs/ssf/dto/AccountStatusDto.java (1)
  • Data (11-22)
src/main/java/com/rcs/ssf/dto/ApiKeyDto.java (2)
src/main/java/com/rcs/ssf/dto/CreateApiKeyRequestDto.java (1)
  • Data (15-41)
src/main/java/com/rcs/ssf/dto/CreateApiKeyResponseDto.java (1)
  • Data (12-24)
src/main/java/com/rcs/ssf/service/ApiKeyService.java (2)
src/main/java/com/rcs/ssf/graphql/SettingsMutation.java (1)
  • Slf4j (31-249)
frontend/src/app/core/services/settings.service.ts (3)
  • generateApiKey (199-209)
  • revokeApiKey (215-223)
  • deleteApiKey (229-237)
🪛 ast-grep (0.40.0)
src/main/java/com/rcs/ssf/service/ETagCacheService.java

[warning] 108-108: Detected SHA1 hash algorithm which is considered insecure. SHA1 is not collision resistant and is therefore not suitable as a cryptographic signature. Instead, use PBKDF2 for password hashing or SHA256 or SHA512 for other hash function applications.
Context: MessageDigest.getInstance("SHA-1")
Note: [CWE-328] Use of Weak Hash. [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(use-of-sha1-java)

src/main/java/com/rcs/ssf/service/AvatarUploadService.java

[warning] 157-157: Detected MD5 hash algorithm which is considered insecure. MD5 is not collision resistant and is therefore not suitable as a cryptographic signature. Use HMAC instead.
Context: "MD5"
Note: [CWE-328] Use of Weak Hash. [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(use-of-md5-java)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (45)
src/main/resources/application.yml (3)

124-125: LGTM! CORS configuration is well-structured.

The CORS configuration appropriately defaults to http://localhost:4200 for development while allowing production override via environment variable. This aligns with the configuration in application-dev.yml and application-production.yml.


161-166: LGTM! Metrics configuration enhances observability.

Enabling cache, JVM, process, and system metrics provides essential operational visibility without performance concerns.


178-187: LGTM! Logging adjustments reduce verbosity appropriately.

Setting these components to INFO level provides a good balance between observability and log volume for production environments.

db/migration/V500__user_settings_profile.sql (3)

4-38: LGTM! Users table alterations are well-structured.

The columns are appropriately typed and the idempotent exception handling ensures safe re-runs. The default value for account_status aligns with the AccountStatus.ACTIVE enum shown in the User entity.


40-76: LGTM! User preferences table is well-designed.

The schema appropriately uses NUMBER(1) for boolean flags, NUMBER(19) for timestamps, and includes proper indexing and cascading deletes. The UNIQUE constraint on user_id ensures one-to-one relationship with users.


103-126: LGTM! API key indexes support efficient queries.

The composite index on (user_id, revoked_at, expires_at) efficiently supports queries for active keys per user, and the single-column index on user_id provides broader query support.

src/main/java/com/rcs/ssf/entity/AccountDeactivationAudit.java (1)

1-48: LGTM! Entity is well-structured with auto-timestamp initialization.

The entity cleanly maps to the audit table schema (once the table is created per the critical issue flagged in the repository review). The special constructor that auto-sets timestamp to the current time is a good defensive pattern.

README.md (4)

191-205: LGTM! CORS documentation is clear and security-conscious.

The documentation appropriately emphasizes the production requirement for CORS configuration and provides practical examples for setting trusted origins.


337-339: LGTM! Avatar and cache configuration is well-documented.

The default values are reasonable (5 MB max size, common image formats, 60-minute cache TTL) and clearly documented.


434-546: LGTM! GraphQL examples are comprehensive and syntactically correct.

The examples demonstrate proper GraphQL query and mutation syntax, covering the full range of user settings operations introduced in this PR.


548-583: LGTM! REST avatar endpoints are well-documented.

The documentation clearly describes the upload, download, and delete operations with appropriate constraints and response examples that align with the configured limits.

src/main/java/com/rcs/ssf/controller/AuthController.java (1)

76-77: LGTM! Defensive truncation prevents audit log failures.

Using AuditService.truncateFailureReason() ensures that exception messages don't exceed database column limits when logging authentication failures, preventing secondary errors during audit logging.

src/main/resources/application-dev.yml (1)

4-6: LGTM! Dev CORS configuration covers common development scenarios.

The allowed origins appropriately include multiple localhost and 127.0.0.1 variants on common frontend development ports (4200 for Angular, 3000 for React, 8080 for general dev).

src/main/resources/application-production.yml (1)

43-45: LGTM! Production CORS requires explicit configuration.

Correctly sources allowed origins from the CORS_ALLOWED_ORIGINS environment variable without a fallback, enforcing explicit configuration in production. The comment provides helpful examples.

src/main/java/com/rcs/ssf/graphql/AuthMutation.java (1)

135-136: LGTM! Defensive improvement for database storage.

The truncation of failure reasons prevents potential database column overflow issues while preserving audit trail information.

src/main/java/com/rcs/ssf/entity/AccountStatus.java (1)

1-22: LGTM! Clean type-safe enum implementation.

The enum provides clear, well-documented account status values with type safety. The Javadoc comments effectively describe each status state.

src/main/java/com/rcs/ssf/config/CorsProperties.java (1)

1-41: LGTM! Well-documented configuration properties class.

The implementation follows Spring Boot best practices with clear documentation and sensible defaults. The Javadoc examples effectively demonstrate configuration usage across different environments.

src/main/java/com/rcs/ssf/service/AuditService.java (1)

18-42: Well-implemented utility method with verified database compatibility.

The truncation logic correctly handles null inputs and preserves meaningful failure information within database constraints. Verification confirms the failure_reason column is defined as VARCHAR2(500) in the database schema, and the method safely truncates to 499 characters max (496 chars + "..." suffix), well within the column limit.

db/migration/V501__api_keys_add_preview.sql (1)

4-13: The error code is correct as-is.

ORA-01430 (numeric code 1430) refers to "column being added already exists in table." In Oracle PL/SQL, SQLCODE returns error codes as negative numbers, so the code correctly checks for SQLCODE = -1430 to catch this exception. No changes required.

src/main/java/com/rcs/ssf/config/SecurityConfig.java (1)

84-94: CORS origin parsing via CorsProperties looks correct; be mindful of empty config and credentials

Using CorsProperties.getAllowedOrigins() and parseAllowedOrigins to drive setAllowedOriginPatterns is a good improvement over hardcoding. A couple of behavioral points to keep in mind:

  • If allowedOrigins is ever set to an empty string, no origin patterns will be configured, effectively blocking all cross‑origin requests.
  • configuration.setAllowCredentials(false); is fine for Authorization‑header–based JWT auth, but will prevent cookie‑based auth from working cross‑origin; if you later introduce cookie sessions, this flag (and matching frontend credentials settings) will need revisiting.

Functionally the change looks sound as long as the new app.cors.allowed-origins (or equivalent) property is wired and documented.

Also applies to: 212-237

src/main/java/com/rcs/ssf/entity/UserPreferences.java (1)

14-75: UserPreferences defaults and constructors look solid

Field-level defaults plus the @PersistenceCreator constructor defensively handling nulls give predictable behavior both for DB hydration and programmatic construction; the convenience (Long userId) ctor matches this pattern. No issues from a persistence or caching standpoint.

src/main/java/com/rcs/ssf/service/UserService.java (1)

224-243: Stronger post‑conditions in updateUser look good

The JDBC branch now explicitly fails when affectedRows == 0, and the R2DBC branch verifies the save by refetching the user and throwing if the record disappeared. This makes update semantics clearer and protects callers from silent no‑ops.

src/main/java/com/rcs/ssf/dto/CreateApiKeyResponseDto.java (1)

12-24: CreateApiKeyResponseDto shape looks consistent and safe

Fields and Lombok annotations line up with the rest of the API key DTOs, and excluding rawKey from toString avoids accidental key leakage in logs. No changes needed here.

src/main/java/com/rcs/ssf/repository/ApiKeyRepository.java (1)

12-24: Repository methods and active/expired semantics are coherent

findActiveByUserId, findAllActive, and deleteExpiredKeys all use consistent conditions on revoked_at and expires_at, matching the ApiKey.isActive()/isExpired() logic in the entity. Signatures look appropriate for R2DBC usage.

src/main/java/com/rcs/ssf/entity/ApiKey.java (1)

14-86: ApiKey entity design aligns with repository and DTOs

Field mappings, constructors, and isRevoked / isExpired / isActive helpers are consistent with the repository queries and DTO surface. Using epoch millis keeps things simple and coherent with the rest of the codebase. No changes needed here.

src/main/java/com/rcs/ssf/service/AvatarUploadService.java (1)

40-77: Upload pipeline and MinIO usage are correct

For known-size uploads, the recommended pattern is stream(inputStream, objectSize, -1) to let the client auto-choose multipart part size. The code implements this correctly with stream(inputStream, file.getSize(), -1).

Validation (non-empty, ≤5MB, allowed MIME types) and key generation are straightforward with no functional issues.

src/main/java/com/rcs/ssf/controller/UserProfileController.java (2)

193-210: LGTM!

The authentication extraction logic appropriately handles different authentication scenarios with a reasonable fallback strategy.


218-227: LGTM!

The content type determination is straightforward and covers the supported avatar formats (JPEG, PNG, WebP) with an appropriate fallback.

src/main/java/com/rcs/ssf/entity/User.java (2)

47-64: LGTM! Defensive enum parsing with appropriate fallback.

The constructor correctly handles potential database inconsistencies by catching IllegalArgumentException when parsing the accountStatus string. Falling back to ACTIVE with a warning is a reasonable strategy for data resilience.


38-45: LGTM! New fields align with PR objectives.

The new fields support avatar management and account lifecycle features as described in the PR objectives.

src/main/resources/graphql/schema.graphqls (3)

12-17: LGTM! Account status enum aligns with entity definition.

The AccountStatus enum matches the Java enum and supports the account lifecycle features.


36-45: LGTM! Settings mutations provide comprehensive user management.

The mutations cover all the key settings operations: avatar, preferences, API keys, account status, and password updates.


64-110: LGTM! Well-structured DTOs for settings features.

The types provide clear contracts for the settings API surface, with appropriate fields for each concern (preferences, API keys, profile, account status).

src/main/java/com/rcs/ssf/dto/UserPreferencesDto.java (2)

19-23: LGTM! Validation on required preference fields.

The @NotBlank constraints appropriately enforce that theme and language must be provided.


25-28: The notification preferences design is intentional and correct as-is.

The DTO's nullable Boolean fields without defaults implement a standard partial-update pattern: null means "don't change this field," while true or false explicitly sets the value. The service layer correctly respects this by checking if (dto.getNotificationEmails() != null) before updating (lines 76–86 of UserPreferencesService). The entity has appropriate defaults (true) applied only when creating new records. No changes needed.

Likely an incorrect or invalid review comment.

frontend/src/app/features/settings/pages/api-keys.component.ts (2)

312-316: Verify clipboard API browser support and permissions.

The navigator.clipboard.writeText() API requires HTTPS (or localhost) and may require user permission in some browsers.

Consider adding fallback handling:

copyKeyToClipboard(): void {
  if (!navigator.clipboard) {
    this.msg.warning('Clipboard access not available. Please copy manually.');
    return;
  }
  
  navigator.clipboard.writeText(this.newGeneratedKey).then(
    () => this.msg.success('API Key copied to clipboard'),
    (err) => {
      console.error('Failed to copy:', err);
      this.msg.error('Failed to copy to clipboard');
    }
  );
}

220-228: LGTM! Form initialization with appropriate validation.

The form setup includes proper validators for key name (required, min length) and expiration days (required, range 1-365).

src/main/java/com/rcs/ssf/service/ETagCacheService.java (3)

107-117: SHA-1 usage is acceptable here (not a security concern).

The static analysis tool flagged SHA-1 as weak, but in this context it's used only for generating cache keys (content-addressed storage), not for cryptographic signatures or authentication. SHA-1 collisions are not a security risk for this use case since:

  1. The cache key is internal to Redis (not exposed to users)
  2. A collision would only result in a cache hit for different content (minor efficiency issue, not a security breach)
  3. SHA-1 is faster than SHA-256 for this non-cryptographic purpose

The actual ETag computation uses SHA-256 (line 87), which is the security-relevant hash.


50-77: LGTM! Robust cache-aside pattern with graceful degradation.

The implementation correctly implements cache-aside with appropriate error handling:

  • Cache misses fall back to computation
  • Redis failures don't break the request
  • Proper logging for observability

140-153: LGTM! Safe Redis connection management.

Using RedisTemplate.execute() with RedisCallback properly manages connection lifecycle, avoiding potential connection leaks.

frontend/src/app/features/settings/pages/notifications.component.ts (2)

220-245: LGTM! Form initialization with sensible defaults.

The forms are initialized with reasonable default values for notification preferences.


247-250: LGTM! Proper subscription cleanup.

Using destroy$ with takeUntil in the submit handlers prevents memory leaks from uncompleted subscriptions.

frontend/src/app/core/services/settings.service.ts (1)

199-209: LGTM! API key generation with proper cache refresh.

The generateApiKey method correctly uses refetchQueries to update the cached API keys list after generation.

src/main/java/com/rcs/ssf/service/AccountService.java (1)

117-142: Account status read helpers look correct and defensive

getAccountStatus and isAccountActive handle null userId, empty repository results, and repository errors gracefully (IllegalArgumentException or false for booleans). The use of defaultIfEmpty(false) plus onErrorReturn(false) in isAccountActive makes the read path robust against both missing users and backend failures.

src/main/java/com/rcs/ssf/service/ApiKeyService.java (1)

49-81: API key generation flow is secure and one‑time‑view friendly

generateApiKey correctly:

  • generates a high-entropy raw key with a fixed prefix,
  • computes a preview from the raw key,
  • hashes the raw key for storage, and
  • only returns the raw key in the response without persisting it.

Logging avoids exposing the raw key. From a lifecycle and security perspective this aligns well with common API-key designs.

Comment thread db/migration/V500__user_settings_profile.sql
Comment thread db/migration/V501__api_keys_add_preview.sql
Comment thread frontend/src/app/core/services/settings.service.ts
Comment thread frontend/src/app/features/settings/pages/preferences.component.ts
Comment thread frontend/src/app/features/settings/pages/profile.component.ts
Comment thread HELP.md
Comment thread src/main/java/com/rcs/ssf/dto/ApiKeyDto.java
Comment thread src/main/java/com/rcs/ssf/service/AccountService.java
Comment thread src/main/java/com/rcs/ssf/service/UserService.java
- Add key_preview column to api_keys table for UI-visible masked previews (e.g., sk_••••••••), avoiding hash-of-hash issues
- Modify deactivateAccount GraphQL mutation to use reasonCode and justification fields
- Add account_deactivation_audit table with indexes for tracking deactivations, including user_id, timestamp, reason_code, and justification
- Refactor CacheMetricsController to extract Caffeine cache entries, reducing code duplication
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

♻️ Duplicate comments (6)
db/migration/V501__api_keys_add_preview.sql (1)

18-27: LGTM: No explicit COMMIT in migration block.

The past review concern about explicit COMMIT interfering with Flyway transaction management has been addressed. The UPDATE block (lines 18-27) correctly relies on Flyway's transaction control without manual commits.

db/migration/V500__user_settings_profile.sql (1)

78-102: LGTM: key_preview column now included in api_keys table.

Line 85 adds the key_preview VARCHAR2(255) column that was missing in the previous review. This resolves the entity-schema mapping issue and aligns with the ApiKey entity's @Column("key_preview") annotation.

frontend/src/app/features/settings/pages/profile.component.ts (2)

16-16: LGTM: NzGridModule now imported for grid directives.

NzGridModule is correctly imported at line 16 and included in the component's imports array at line 40, resolving the past review issue where nz-row and nz-col directives would not have been available.

Also applies to: 40-40


244-266: LGTM: isUpdatingPassword flag now properly managed.

The password change submission now includes the complete updatePassword call with both success and error handlers that reset the isUpdatingPassword flag (lines 259, 263), resolving the past review issue where the button would remain stuck in the "Updating..." state.

frontend/src/app/core/services/settings.service.ts (1)

38-51: Fix multiple GraphQL contract mismatches in SettingsService

Several operations here don’t match the server schema / resolvers and will fail at validation time:

  1. UPDATE_USER_PREFERENCES mutation (lines 38–51)

    • Schema: updateUserPreferences(input: UserPreferencesInput!): UserPreferences!
    • Java resolver: updateUserPreferences(@Argument UserPreferencesInput input)
    • Current query uses updateUserPreferences(preferences: $preferences) and selects
      emailNotifications, pushNotifications, apiKeyNotifications, accountActivityNotifications
      which are not fields on UserPreferences (schema uses notificationEmails, notificationPush,
      notificationLoginAlerts, notificationSecurityUpdates).

    Fix:

    • Change the argument name to input:
      mutation UpdateUserPreferences($preferences: UserPreferencesInput!) {
        updateUserPreferences(input: $preferences) {
          userId
          theme
          language
          notificationEmails
          notificationPush
          notificationLoginAlerts
          notificationSecurityUpdates
          updatedAt
        }
      }
    • Keep variables: { preferences } in TS; the variable name doesn’t need to equal the schema arg name.
  2. GET_API_KEYS and REVOKE_API_KEY (lines 56–69, 91–99)

    • Schema type ApiKeyInfo exposes isActive: Boolean! (plus id, keyName, keyPreview, timestamps).
    • These queries request a status field, which is not in the schema.

    Fix:

    • Either add status to ApiKeyInfo in the schema and map it from ApiKeyDto, or
    • Change the queries to use isActive and compute a status string client‑side if needed.
  3. GET_ACCOUNT_STATUS (lines 113–121)

    • Schema: getAccountStatus: AccountStatus! (enum).
    • Resolver: returns Mono<String>/enum value, not an object.
    • Query currently treats it as an object: getAccountStatus { userId status deactivatedAt }.

    Fix:

    • Update the query to:
      query GetAccountStatus {
        getAccountStatus
      }
    • And adjust getAccountStatus() in TS to treat the result as the enum name.

    If you decide instead to return an object, change the schema + resolver to AccountStatusResponse! and keep the object shape here.

  4. UPDATE_PASSWORD (lines 152–159)

    • Schema: updatePassword(currentPassword: String!, newPassword: String!): Boolean!
    • Resolver: returns Boolean.
    • Query expects an object { success, message }.

    Fix:

    • Either:
      • Keep schema/resolver as Boolean! and change the mutation to:
        mutation UpdatePassword($currentPassword: String!, $newPassword: String!) {
          updatePassword(currentPassword: $currentPassword, newPassword: $newPassword)
        }
        and have TS treat the result as a simple boolean; or
      • Change schema + resolver to return a dedicated payload type with success/message
        and keep the current shape.

Until these are corrected, the affected operations will not work end‑to‑end.

Check that the GraphQL server’s introspected schema for UserPreferences, ApiKeyInfo, AccountStatus, and updatePassword matches the field/argument names used above (especially `notification*` vs `*Notifications`, `isActive` vs `status`, and the return type of `updatePassword`).

Also applies to: 56-69, 91-99, 113-121, 126-147, 152-159

frontend/src/app/features/settings/pages/preferences.component.ts (1)

250-269: Harden loadPreferences against bad/partial localStorage data

loadPreferences blindly JSON.parses and patches all fields, so:

  • Corrupted JSON in localStorage will throw and break the component.
  • Missing fields (e.g., from older versions) get patched as undefined, overwriting your in-code defaults.

Guard both by catching parse errors and only patching controls when the stored value is not undefined:

  private loadPreferences(): void {
-    const stored = localStorage.getItem('user-preferences');
-    if (stored) {
-      const preferences = JSON.parse(stored);
-      this.appearanceForm.patchValue({
-        theme: preferences.theme,
-        language: preferences.language
-      });
-      this.notificationForm.patchValue({
-        notificationEmails: preferences.notificationEmails,
-        notificationPush: preferences.notificationPush,
-        notificationLoginAlerts: preferences.notificationLoginAlerts,
-        notificationSecurityUpdates: preferences.notificationSecurityUpdates
-      });
-      
-      // Apply theme when preferences are loaded
-      if (preferences.theme !== undefined) {
-        this.applyTheme(preferences.theme);
-      }
-    }
+    const stored = localStorage.getItem('user-preferences');
+    if (!stored) {
+      return;
+    }
+    let preferences: any;
+    try {
+      preferences = JSON.parse(stored);
+    } catch {
+      // Ignore invalid JSON and keep defaults
+      return;
+    }
+
+    const appearancePatch: any = {};
+    if (preferences.theme !== undefined) {
+      appearancePatch.theme = preferences.theme;
+    }
+    if (preferences.language !== undefined) {
+      appearancePatch.language = preferences.language;
+    }
+    this.appearanceForm.patchValue(appearancePatch);
+
+    const notificationPatch: any = {};
+    if (preferences.notificationEmails !== undefined) {
+      notificationPatch.notificationEmails = preferences.notificationEmails;
+    }
+    if (preferences.notificationPush !== undefined) {
+      notificationPatch.notificationPush = preferences.notificationPush;
+    }
+    if (preferences.notificationLoginAlerts !== undefined) {
+      notificationPatch.notificationLoginAlerts = preferences.notificationLoginAlerts;
+    }
+    if (preferences.notificationSecurityUpdates !== undefined) {
+      notificationPatch.notificationSecurityUpdates = preferences.notificationSecurityUpdates;
+    }
+    this.notificationForm.patchValue(notificationPatch);
+
+    if (preferences.theme !== undefined) {
+      this.applyTheme(preferences.theme);
+    }
   }
🧹 Nitpick comments (4)
src/main/java/com/rcs/ssf/controller/CacheMetricsController.java (2)

35-69: Reduce duplication by reusing getCaffeineCacheEntries() in getCaffeineStats

getCaffeineStats and getCaffeineCacheEntries both iterate over cacheManager.getCacheNames(), unwrap the native cache, and filter by Caffeine type. This is correct but duplicated and slightly increases maintenance cost.

You can reuse getCaffeineCacheEntries() inside getCaffeineStats() to centralize the unwrapping logic:

-    @GetMapping("/caffeine")
-    public Map<String, Object> getCaffeineStats() {
-        Map<String, Object> allStats = new HashMap<>();
-
-        for (String cacheName : cacheManager.getCacheNames()) {
-            org.springframework.cache.Cache cache = cacheManager.getCache(cacheName);
-            if (cache != null) {
-                Object nativeCache = cache.getNativeCache();
-                if (nativeCache instanceof Cache) {
-                    @SuppressWarnings("unchecked")
-                    Cache<Object, Object> caffeineCache = (Cache<Object, Object>) nativeCache;
-
-                    var stats = caffeineCache.stats();
-
-                    Map<String, Object> cacheStats = new HashMap<>();
-                    cacheStats.put("stats", stats.toString());
-                    cacheStats.put("estimatedSize", caffeineCache.estimatedSize());
-                    cacheStats.put("hitCount", stats.hitCount());
-                    cacheStats.put("missCount", stats.missCount());
-                    cacheStats.put("loadSuccessCount", stats.loadSuccessCount());
-                    cacheStats.put("loadFailureCount", stats.loadFailureCount());
-                    cacheStats.put("evictionCount", stats.evictionCount());
-                    cacheStats.put("hitRate", stats.hitRate());
-
-                    allStats.put(cacheName, cacheStats);
-                }
-            }
-        }
-
-        return allStats;
-    }
+    @GetMapping("/caffeine")
+    public Map<String, Object> getCaffeineStats() {
+        Map<String, Object> allStats = new HashMap<>();
+
+        for (CacheEntry entry : getCaffeineCacheEntries()) {
+            var stats = entry.cache().stats();
+
+            Map<String, Object> cacheStats = new HashMap<>();
+            cacheStats.put("stats", stats.toString());
+            cacheStats.put("estimatedSize", entry.cache().estimatedSize());
+            cacheStats.put("hitCount", stats.hitCount());
+            cacheStats.put("missCount", stats.missCount());
+            cacheStats.put("loadSuccessCount", stats.loadSuccessCount());
+            cacheStats.put("loadFailureCount", stats.loadFailureCount());
+            cacheStats.put("evictionCount", stats.evictionCount());
+            cacheStats.put("hitRate", stats.hitRate());
+
+            allStats.put(entry.cacheName(), cacheStats);
+        }
+
+        return allStats;
+    }

Also applies to: 167-183


112-201: Solid summary aggregation; narrow computeOverallHitRate return type

The summary endpoint correctly reuses getCaffeineCacheEntries(), aggregates totals, and clearly defines hit‑rate semantics for “no data” and “perfect hit rate” cases; the logic looks good.

Since all branches of computeOverallHitRate return String, you can simplify the signature to return String instead of Object for better type clarity (and a tighter JSON contract):

-    private Object computeOverallHitRate(long totalHits, long totalMisses) {
+    private String computeOverallHitRate(long totalHits, long totalMisses) {

No other changes are required.

src/main/java/com/rcs/ssf/controller/UserProfileController.java (1)

106-147: Consider streaming response instead of loading entire avatar into memory.

Line 115 calls is.readAllBytes() which loads the entire avatar file into memory. While the upload limit is 5MB (manageable), under high concurrency this could cause memory pressure. Consider streaming the response directly using InputStreamResource or similar to improve scalability:

return Mono.fromCallable(() -> avatarUploadService.downloadAvatar(avatarKey))
    .subscribeOn(Schedulers.boundedElastic())
    .flatMap(avatarDataOpt -> {
      if (avatarDataOpt.isEmpty()) {
        return Mono.just((ResponseEntity<?>) ResponseEntity.notFound().build());
      }
      
      InputStream stream = avatarDataOpt.get();
      String contentType = determineContentType(avatarKey);
      InputStreamResource resource = new InputStreamResource(stream);
      
      return Mono.just((ResponseEntity<?>) ResponseEntity.ok()
          .header("Content-Type", contentType)
          .header("Cache-Control", "public, max-age=86400")
          .body(resource));
    })
    // ... error handling

This approach streams the file directly to the client without buffering in memory.

src/main/java/com/rcs/ssf/graphql/SettingsMutation.java (1)

160-171: Reuse AccountConstants.DEFAULT_REASON_CODE instead of duplicating "USER_INITIATED"

Here:

reason.setReasonCode(reasonCode != null ? reasonCode : "USER_INITIATED");

you already centralize the default in AccountConstants.DEFAULT_REASON_CODE (used in AccountService). To avoid drift between layers, wire this through the constant instead of duplicating the literal.

-        DeactivationReasonDto reason = new DeactivationReasonDto();
-        reason.setReasonCode(reasonCode != null ? reasonCode : "USER_INITIATED");
+        DeactivationReasonDto reason = new DeactivationReasonDto();
+        reason.setReasonCode(reasonCode != null ? reasonCode : AccountConstants.DEFAULT_REASON_CODE);

(You’ll need to import AccountConstants here.)

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d42a41 and 5635b58.

📒 Files selected for processing (20)
  • HELP.md (1 hunks)
  • db/migration/V500__user_settings_profile.sql (1 hunks)
  • db/migration/V501__api_keys_add_preview.sql (1 hunks)
  • frontend/src/app/core/services/settings.service.ts (1 hunks)
  • frontend/src/app/features/settings/pages/notifications.component.ts (1 hunks)
  • frontend/src/app/features/settings/pages/preferences.component.ts (1 hunks)
  • frontend/src/app/features/settings/pages/profile.component.ts (1 hunks)
  • src/main/java/com/rcs/ssf/controller/CacheMetricsController.java (1 hunks)
  • src/main/java/com/rcs/ssf/controller/UserProfileController.java (1 hunks)
  • src/main/java/com/rcs/ssf/dto/ApiKeyDto.java (1 hunks)
  • src/main/java/com/rcs/ssf/dto/CreateApiKeyRequestDto.java (1 hunks)
  • src/main/java/com/rcs/ssf/graphql/SettingsMutation.java (1 hunks)
  • src/main/java/com/rcs/ssf/service/AccountService.java (1 hunks)
  • src/main/java/com/rcs/ssf/service/ApiKeyService.java (1 hunks)
  • src/main/java/com/rcs/ssf/service/AvatarUploadService.java (1 hunks)
  • src/main/java/com/rcs/ssf/service/UserPreferencesService.java (1 hunks)
  • src/main/java/com/rcs/ssf/service/UserService.java (3 hunks)
  • src/main/java/com/rcs/ssf/util/AccountConstants.java (1 hunks)
  • src/main/resources/graphql/schema.graphqls (4 hunks)
  • src/test/java/com/rcs/ssf/graphql/AdminMutationIntegrationTest.java (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/com/rcs/ssf/dto/CreateApiKeyRequestDto.java
  • src/main/java/com/rcs/ssf/dto/ApiKeyDto.java
🧰 Additional context used
🧬 Code graph analysis (11)
frontend/src/app/features/settings/pages/profile.component.ts (1)
frontend/src/app/core/services/auth.service.ts (1)
  • User (26-30)
src/main/java/com/rcs/ssf/controller/CacheMetricsController.java (1)
src/main/java/com/rcs/ssf/controller/ETagMetricsController.java (1)
  • RestController (13-37)
src/main/java/com/rcs/ssf/graphql/SettingsMutation.java (5)
src/main/java/com/rcs/ssf/security/AuthenticatedUser.java (1)
  • AuthenticatedUser (9-24)
src/main/java/com/rcs/ssf/service/AccountService.java (1)
  • Slf4j (22-157)
src/main/java/com/rcs/ssf/service/ApiKeyService.java (1)
  • Slf4j (24-337)
src/main/java/com/rcs/ssf/service/UserPreferencesService.java (1)
  • Slf4j (22-135)
src/main/java/com/rcs/ssf/service/UserService.java (1)
  • Slf4j (36-447)
src/main/java/com/rcs/ssf/service/UserPreferencesService.java (2)
src/main/java/com/rcs/ssf/graphql/SettingsMutation.java (1)
  • Slf4j (31-253)
src/main/java/com/rcs/ssf/service/ETagCacheService.java (1)
  • Service (24-159)
src/main/java/com/rcs/ssf/service/AvatarUploadService.java (1)
frontend/src/app/core/services/settings.service.ts (1)
  • uploadAvatar (293-306)
frontend/src/app/features/settings/pages/preferences.component.ts (2)
frontend/src/app/features/settings/pages/notifications.component.ts (1)
  • Component (20-371)
frontend/src/app/features/settings/settings.component.ts (1)
  • Component (16-99)
src/main/java/com/rcs/ssf/controller/UserProfileController.java (3)
src/main/java/com/rcs/ssf/dto/AvatarUploadResponseDto.java (1)
  • AvatarUploadResponseDto (6-51)
frontend/src/app/core/services/auth.service.ts (1)
  • User (26-30)
frontend/src/app/core/services/settings.service.ts (1)
  • uploadAvatar (293-306)
src/main/java/com/rcs/ssf/service/UserService.java (1)
frontend/src/app/core/services/settings.service.ts (1)
  • updatePassword (279-286)
src/main/java/com/rcs/ssf/service/ApiKeyService.java (1)
src/main/java/com/rcs/ssf/graphql/SettingsMutation.java (1)
  • Slf4j (31-253)
src/main/java/com/rcs/ssf/service/AccountService.java (2)
src/main/java/com/rcs/ssf/util/AccountConstants.java (1)
  • AccountConstants (7-41)
src/main/java/com/rcs/ssf/graphql/SettingsMutation.java (1)
  • Slf4j (31-253)
frontend/src/app/features/settings/pages/notifications.component.ts (2)
frontend/src/app/features/settings/pages/preferences.component.ts (1)
  • Component (20-289)
frontend/src/app/features/settings/settings.component.ts (1)
  • Component (16-99)
🪛 ast-grep (0.40.0)
src/main/java/com/rcs/ssf/service/AvatarUploadService.java

[warning] 173-173: Detected MD5 hash algorithm which is considered insecure. MD5 is not collision resistant and is therefore not suitable as a cryptographic signature. Use HMAC instead.
Context: "MD5"
Note: [CWE-328] Use of Weak Hash. [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(use-of-md5-java)

🔇 Additional comments (10)
src/main/java/com/rcs/ssf/controller/CacheMetricsController.java (1)

15-33: Clear controller design and security documentation

Constructor injection, actuator‑like pathing, and the explicit security note align well with the existing ETagMetricsController pattern; no issues here.

src/main/java/com/rcs/ssf/service/UserService.java (2)

57-58: Password field now correctly hydrated for JDBC mode.

This resolves the critical issue from the previous review where updatePassword would fail in JDBC mode due to null password. The mapper now properly reads and sets the password field with SQL NULL handling.


227-244: LGTM: Improved error handling in both JDBC and R2DBC paths.

The JDBC path now explicitly checks affectedRows and throws a descriptive error when no rows are updated, while the R2DBC path refetches the user after save to verify persistence. Both changes improve reliability and error detection for concurrent modifications.

src/main/java/com/rcs/ssf/service/AvatarUploadService.java (2)

172-181: MD5 usage is acceptable here for non-cryptographic filename generation.

The static analysis warning about MD5 being cryptographically weak is valid in general, but in this context MD5 is used only to generate unique object keys for avatar filenames, not for security-critical operations like password hashing or signatures. This usage is appropriate and efficient.


92-117: LGTM: Robust error handling using structured error codes.

The downloadAvatar method correctly uses ErrorResponseException.errorResponse().code() and HTTP status codes for error detection rather than brittle message string matching. The distinction between "not found" (returns empty) and other errors (propagates exception) is appropriate for the caller's needs.

src/main/java/com/rcs/ssf/controller/UserProfileController.java (1)

51-90: LGTM: Proper reactive flow for avatar upload with error handling.

The upload endpoint correctly uses boundedElastic() for the blocking MinIO operation, updates the user reactively, and provides comprehensive error handling with appropriate HTTP status codes.

HELP.md (2)

102-106: LGTM: Comprehensive troubleshooting guidance for new features.

The new troubleshooting entries clearly document common avatar upload, API key, preferences cache, and account deactivation issues with actionable solutions. This will help developers quickly resolve configuration and usage problems.


231-240: No action required—documentation matches schema.

The GraphQL schema at src/main/resources/graphql/schema.graphqls:43 confirms the deactivateAccount mutation signature matches the documented parameters exactly:

deactivateAccount(reasonCode: String, justification: String): AccountStatusResponse!

The documentation in HELP.md is accurate and consistent with the schema definition.

db/migration/V500__user_settings_profile.sql (1)

4-38: LGTM: Idempotent user table extensions with proper error handling.

The migration safely extends the users table with avatar_key, account_status, and account_deactivated_at columns using exception handling to gracefully handle already-existing columns. The SQLCODE checks are appropriate for Oracle.

src/main/java/com/rcs/ssf/util/AccountConstants.java (1)

1-41: LGTM: Well-structured constants class for account operations.

The utility class follows standard patterns with a private constructor and clearly documented reason codes. The constants provide a centralized, type-safe way to reference deactivation reasons across the account management feature.

Comment thread frontend/src/app/features/settings/pages/notifications.component.ts
Comment thread src/main/java/com/rcs/ssf/controller/CacheMetricsController.java
Comment thread src/main/java/com/rcs/ssf/graphql/SettingsMutation.java
Comment thread src/main/java/com/rcs/ssf/service/AccountService.java
Comment thread src/main/java/com/rcs/ssf/service/ApiKeyService.java
Comment thread src/main/java/com/rcs/ssf/service/UserPreferencesService.java
Comment thread src/main/java/com/rcs/ssf/service/UserService.java
Comment thread src/test/java/com/rcs/ssf/graphql/AdminMutationIntegrationTest.java Outdated
@zlovtnik zlovtnik merged commit 2205c3f into main Nov 21, 2025
2 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant