-
Notifications
You must be signed in to change notification settings - Fork 16
feat: move activation code logic into the API #1369
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughThis update introduces a comprehensive activation code and customization system for Unraid, spanning backend, frontend, and plugin layers. The backend adds new GraphQL types, queries, services, and modules for activation code handling, theming, and partner branding. File modification classes are introduced for injecting banners, logos, and modal dialogs into the Unraid web GUI. The frontend is refactored to use new Pinia stores and GraphQL queries for activation code and partner info, updating modal and logo display logic. Legacy Bash/PHP activation scripts and related plugin code are removed, consolidating customization logic within the API. Supporting configuration, environment, and test files are updated accordingly. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant WebUI
participant API
participant CustomizationService
participant FileModifier
participant FS
User->>WebUI: Loads page
WebUI->>API: GraphQL query (customization, partner info, theme)
API->>CustomizationService: Fetch activation code and partner info
CustomizationService->>FS: Read activation JSON, assets, config
CustomizationService-->>API: Return activation code, partner info, theme
API-->>WebUI: Respond with customization data
WebUI->>WebUI: Update modal, logo, theme using Pinia stores
User->>WebUI: Interacts with activation modal
Possibly related PRs
Suggested reviewers
Poem
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
🔭 Outside diff range comments (4)
api/src/unraid-api/unraid-file-modifier/modifications/patches/sso.patch (2)
20-23:⚠️ Potential issueEscape literal dots in JWT regex
The
.characters in the regex currently match any character, so malformed tokens such asabcXdefYghiwould still pass the format check. Escape the dots to ensure you only allow the JWT’s three-part structure separated by literal periods.- if (!preg_match('/^[A-Za-z0-9-_]+.[A-Za-z0-9-_]+.[A-Za-z0-9-_]+$/', $password)) { + if (!preg_match('/^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/', $password)) {
25-30: 🛠️ Refactor suggestionPassing the token via the command line leaks secrets
exec("/etc/rc.d/rc.unraid-api sso validate-token $safePassword …")puts the full JWT in the process list, making it visible viapsto any local user. Instead, pipe the token onstdinor use an environment variable that is cleared immediately after use.Example sketch:
$process = proc_open( '/etc/rc.d/rc.unraid-api sso validate-token -', [['pipe','r'], ['pipe','w'], ['pipe','w']], $pipes ); fwrite($pipes[0], $password); fclose($pipes[0]); $output = stream_get_contents($pipes[1]); $code = proc_close($process);api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php (2)
15-18:⚠️ Potential issueEscape dots in JWT regex to avoid false positives
In the pattern
'/^[A-Za-z0-9-_]+.[A-Za-z0-9-_]+.[A-Za-z0-9-_]+$/'
the dot (.) matches any character, so an attacker could smuggle arbitrary content between the token parts and still pass the check.- if (!preg_match('/^[A-Za-z0-9-_]+.[A-Za-z0-9-_]+.[A-Za-z0-9-_]+$/', $password)) { + if (!preg_match('/^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/', $password)) {Escaping the dots limits matches to the canonical
header.payload.signaturestructure.
15-16: 🛠️ Refactor suggestionHard-coding “> 800” may reject valid tokens
Most JWTs are ≤ 1 kB and many are ~300 B. Relying on
strlen($password) > 800as the sole indicator of an SSO token risks false negatives (shorter but valid tokens) and fragile behaviour if token sizes change. Consider detecting tokens exclusively via the regex or a dedicatedAuthorization:prefix.
🧹 Nitpick comments (10)
api/src/unraid-api/graph/resolvers/customization/activation-code.dto.ts (1)
5-10: Consider typing and enhancing the sanitizeString functionThe sanitization function has correct logic for removing potentially harmful characters, but it could benefit from more specific typing.
-const sanitizeString = (value: any): any => { +const sanitizeString = (value: unknown): string | unknown => { if (typeof value === 'string') { return value.replace(/[\\"']/g, ''); // Remove backslashes, double quotes, and single quotes } return value; };api/src/unraid-api/unraid-file-modifier/modifications/case-model-copier.modification.ts (1)
16-21: Accessing the Redux store inside the constructor may race with boot-strappingIf
CaseModelCopierModificationis instantiated before thepathsslice is populated,paths.activationBasewill beundefined, causingpath.join(undefined, 'assets')to throw. Lazily read the store inapply()or inject thePathsServiceto guarantee initialization.- constructor() { - const paths = store.getState().paths; - this.assetsDir = path.join(paths.activationBase, 'assets'); - this.webguiImagesDir = paths.webguiImagesBase; - this.logger.debug('CaseModelCopierModification initialized with paths from store.'); - } + constructor() { + this.logger.debug('CaseModelCopierModification constructed.'); + } + + private initPaths() { + const paths = store.getState().paths; + this.assetsDir ??= path.join(paths.activationBase, 'assets'); + this.webguiImagesDir ??= paths.webguiImagesBase; + }api/src/unraid-api/unraid-file-modifier/modifications/partner-logo-copier.modification.ts (1)
36-38: Specify the symlink type for Windows compatibility
fs.symlink()defaults to “file” on POSIX but requires an explicit'file'type on Windows; otherwise the call may fail under NTFS. Adding the third argument keeps the code portable.- await fs.symlink(partnerLogo, linkDest); + await fs.symlink(partnerLogo, linkDest, 'file');api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php (1)
498-501: DeclarecookieEnabledwithlet/constImplicit globals hinder maintainability and can collide with other scripts:
- cookieEnabled = document.cookie.indexOf("cookietest=")!=-1; + const cookieEnabled = document.cookie.indexOf("cookietest=") !== -1;api/src/unraid-api/unraid-file-modifier/modifications/set-password-modal.modification.ts (3)
13-18: UseoverridePathconsistently when reading the file
generatePatch()honoursoverridePathonly when creating the diff, but still readsthis.filePath. Reading the override first prevents mismatched content in test scenarios.- const fileContent = await readFile(this.filePath, 'utf-8'); + const sourcePath = overridePath ?? this.filePath; + const fileContent = await readFile(sourcePath, 'utf-8');
21-36: Gracefully handle missing target file
readFile()will reject if the file is absent (e.g. older Unraid versions), bubbling an unhandled promise rejection. Wrap withtry/catchand returnshouldApply: falseplus the error reason to keep the modifier service resilient.
38-47: Body-tag search is case-sensitive
</body>is matched literally; many legacy Unraid files use</BODY>. A case-insensitive search (/iflag ortoLowerCase()) would make the injection more robust.api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch (1)
33-36: Add alt-text when replacing the logo for accessibilityThe original
<a>element rendered an inline SVG (self-describing). The new include outputs an<img>tag without guaranteedalttext. Ensurepartner-logo.phpprovides meaningfulaltso screen-reader users aren’t left in the dark.api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)
1-29: New service for handling activation-based customizations.A well-structured service class has been created to manage Unraid system customizations based on activation data. The class properly uses dependency injection and logging.
However, there's an unnecessary empty constructor that can be removed:
-constructor() {}🧰 Tools
🪛 Biome (1.9.4)
[error] 29-29: This constructor is unnecessary.
Unsafe fix: Remove the unnecessary constructor.
(lint/complexity/noUselessConstructor)
72-81: Formatting issue in log message.The log message exceeds the line length limits according to static analysis.
- this.logger.log(`Activation directory ${this.activationDir} not found. Skipping activation setup.`); + this.logger.log( + `Activation directory ${this.activationDir} not found. Skipping activation setup.` + );🧰 Tools
🪛 GitHub Check: Build API
[failure] 77-77:
ReplaceActivation·directory·${this.activationDir}·not·found.·Skipping·activation·setup.with⏎························Activation·directory·${this.activationDir}·not·found.·Skipping·activation·setup.⏎····················🪛 GitHub Check: Test API
[failure] 77-77:
ReplaceActivation·directory·${this.activationDir}·not·found.·Skipping·activation·setup.with⏎························Activation·directory·${this.activationDir}·not·found.·Skipping·activation·setup.⏎····················
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (26)
.cursor/rules/api-rules.mdc(1 hunks)api/dev/Unraid.net/myservers.cfg(1 hunks)api/dev/ident.cfg(1 hunks)api/src/__test__/store/modules/paths.test.ts(2 hunks)api/src/customization/activation-code.dto.ts(1 hunks)api/src/store/modules/paths.ts(2 hunks)api/src/unraid-api/graph/resolvers/customization/activation-code.dto.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.service.ts(1 hunks)api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php(14 hunks)api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php.last-download-time(1 hunks)api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php(1 hunks)api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php.last-download-time(1 hunks)api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/Notifications.page.last-download-time(1 hunks)api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/auth-request.php.last-download-time(1 hunks)api/src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts(2 hunks)api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php(14 hunks)api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php(2 hunks)api/src/unraid-api/unraid-file-modifier/modifications/case-model-copier.modification.ts(1 hunks)api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts(1 hunks)api/src/unraid-api/unraid-file-modifier/modifications/log-rotate.modification.ts(1 hunks)api/src/unraid-api/unraid-file-modifier/modifications/log-viewer.modification.ts(1 hunks)api/src/unraid-api/unraid-file-modifier/modifications/partner-logo-copier.modification.ts(1 hunks)api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch(4 hunks)api/src/unraid-api/unraid-file-modifier/modifications/patches/sso.patch(1 hunks)api/src/unraid-api/unraid-file-modifier/modifications/set-password-modal.modification.ts(1 hunks)api/src/unraid-api/unraid-file-modifier/unraid-file-modifier.service.ts(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch (3)
Learnt from: elibosley
PR: unraid/api#1101
File: api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch:6-20
Timestamp: 2025-01-31T22:01:41.842Z
Learning: The default-page-layout.patch is used to remove the old jGrowl notification system from Unraid pages, as notifications are handled by a new system implemented on a different page.
Learnt from: elibosley
PR: unraid/api#1101
File: api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch:6-20
Timestamp: 2025-01-31T22:01:41.842Z
Learning: The default-page-layout.patch removes the old jGrowl notification system and is complemented by the unraid-toaster component implementation. The new system is added through the DefaultPageLayout modification which inserts the toaster component with proper position configuration based on user preferences.
Learnt from: elibosley
PR: unraid/api#1101
File: api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch:30-54
Timestamp: 2025-01-31T22:01:02.725Z
Learning: The removal of jGrowl notifications from DefaultPageLayout.php is intentional as notifications are now handled on a separate page as part of the architectural design to override existing Unraid pages.
🧬 Code Graph Analysis (5)
api/src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts (1)
api/src/unraid-api/unraid-file-modifier/modifications/sso.modification.ts (1)
SSOFileModification(8-98)
api/src/unraid-api/unraid-file-modifier/modifications/set-password-modal.modification.ts (1)
api/src/unraid-api/unraid-file-modifier/file-modification.ts (1)
ShouldApplyWithReason(8-11)
api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php (2)
api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php (6)
readFromFile(71-83)appendToFile(85-94)writeToFile(96-105)cleanupFails(115-139)isValidTimeStamp(108-113)verifyUsernamePassword(141-153)plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.plugin.manager/include/UnraidCheck.php (1)
_(108-108)
api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php (2)
api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php (6)
readFromFile(20-32)appendToFile(34-43)writeToFile(45-54)cleanupFails(64-88)isValidTimeStamp(57-62)verifyUsernamePassword(90-102)plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.plugin.manager/include/UnraidCheck.php (1)
_(108-108)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (4)
api/src/unraid-api/graph/resolvers/customization/activation-code.dto.ts (1)
ActivationCodeDto(12-76)packages/unraid-api-plugin-connect/src/helpers/utils.ts (1)
fileExists(5-8)api/src/store/index.ts (1)
getters(18-29)api/src/core/utils/clients/emcmd.ts (1)
emcmd(12-41)
🪛 GitHub Check: Build API
api/src/customization/activation-code.dto.ts
[failure] 1-1:
Delete ·
[failure] 1-1:
Too many blank lines at the beginning of file. Max of 0 allowed
[failure] 1-1:
Newline required at end of file but not found
api/src/unraid-api/graph/resolvers/customization/activation-code.dto.ts
[failure] 4-4:
Delete ⏎
[failure] 4-4:
More than 1 blank line not allowed
api/src/store/modules/paths.ts
[failure] 22-22:
Replace 'dynamixCaseModelConfig' with dynamixCaseModelConfig
[failure] 78-78:
Replace 'activationBase' with activationBase
[failure] 79-79:
Replace 'webguiImagesBase' with webguiImagesBase
[failure] 80-80:
Replace 'identConfig' with identConfig
api/src/unraid-api/graph/resolvers/customization/customization.service.ts
[failure] 77-77:
Replace Activation·directory·${this.activationDir}·not·found.·Skipping·activation·setup. with ⏎························Activation·directory·${this.activationDir}·not·found.·Skipping·activation·setup.⏎····················
🪛 GitHub Check: Test API
api/src/customization/activation-code.dto.ts
[failure] 1-1:
Delete ·
[failure] 1-1:
Too many blank lines at the beginning of file. Max of 0 allowed
[failure] 1-1:
Newline required at end of file but not found
api/src/unraid-api/graph/resolvers/customization/activation-code.dto.ts
[failure] 4-4:
Delete ⏎
[failure] 4-4:
More than 1 blank line not allowed
api/src/store/modules/paths.ts
[failure] 22-22:
Replace 'dynamixCaseModelConfig' with dynamixCaseModelConfig
[failure] 78-78:
Replace 'activationBase' with activationBase
[failure] 79-79:
Replace 'webguiImagesBase' with webguiImagesBase
[failure] 80-80:
Replace 'identConfig' with identConfig
api/src/unraid-api/graph/resolvers/customization/customization.service.ts
[failure] 77-77:
Replace Activation·directory·${this.activationDir}·not·found.·Skipping·activation·setup. with ⏎························Activation·directory·${this.activationDir}·not·found.·Skipping·activation·setup.⏎····················
🪛 Biome (1.9.4)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts
[error] 29-29: This constructor is unnecessary.
Unsafe fix: Remove the unnecessary constructor.
(lint/complexity/noUselessConstructor)
⏰ Context from checks skipped due to timeout of 90000ms (2)
- GitHub Check: Build Web App
- GitHub Check: Cloudflare Pages
🔇 Additional comments (41)
api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/auth-request.php.last-download-time (1)
1-1: Approve fixture timestamp update.
The timestamp was bumped to reflect the latest download snapshot. No action needed.api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/Notifications.page.last-download-time (1)
1-1: Approve fixture timestamp update.
Updated notification fixture timestamp to match the new UI changes. All good.api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php.last-download-time (1)
1-1: Approve fixture timestamp update.
The DefaultPageLayout fixture timestamp is updated to align with UI customization tests. Looks good.api/dev/Unraid.net/myservers.cfg (1)
2-2: Verify version change.
[api].versionwas downgraded from"4.6.6"to"4.4.1". Please confirm this is intentional and compatible with downstream systems.api/src/unraid-api/unraid-file-modifier/modifications/log-rotate.modification.ts (1)
10-10: Export style changed to default exportThe change from named export to default export aligns with the broader refactoring mentioned in the summary, where modification classes are now dynamically imported using
import.meta.glob.api/dev/ident.cfg (1)
1-36: Configuration file looks goodThis new configuration file contains appropriate default values for system and network settings. It will be used by the CustomizationService to modify identity parameters based on activation data.
api/src/__test__/store/modules/paths.test.ts (1)
19-36: Test updated to include new path keysThe snapshot test has been correctly updated to include the new path keys added to the store.
api/src/unraid-api/graph/resolvers/customization/activation-code.dto.ts (1)
12-76: Well-structured DTO with appropriate validationsThe DTO is well-designed with proper validations for each field. The use of
@IsOptional()for all fields makes sense for a configuration where not all fields may be provided. The string sanitization ensures that potentially harmful characters are removed.api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php.last-download-time (1)
1-1: Timestamp updated for login.php fixture.This timestamp update indicates the login.php test fixture was refreshed, likely reflecting the broader authentication system changes mentioned in the summary.
api/src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts (2)
11-11: Import style aligned with module export change.The import statement has been updated to use a default import for
LogRotateModification, reflecting changes in the export style of that class.
39-39: Property order adjusted for consistency.The order of properties in the
SSOFileModificationtest case has been adjusted for better readability, with the class definition now appearing before the file URL.api/src/unraid-api/unraid-file-modifier/modifications/log-viewer.modification.ts (1)
10-10: Changed to default export for consistency.The export has been changed from a named export to a default export to align with a broader pattern change in file modification classes. This supports the dynamic loading of modifications implemented in the
unraid-file-modifier.service.tsfile..cursor/rules/api-rules.mdc (2)
3-3: Expanded glob pattern to include nested directories.The glob pattern has been broadened from
api/*toapi/**/*,api/*to include all files recursively within theapidirectory, ensuring new nested files are properly covered by these rules.
11-11: Added new guideline for dependency mocking.Added a sensible guideline to prefer not mocking simple dependencies, which should lead to more robust tests.
api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php (3)
695-695: Partner logo replacement implemented correctlyThe static Unraid logo link has been replaced with a PHP include for a partner logo. This is a good implementation that enables dynamic partner branding.
1294-1299: Improved page refresh behavior for extended viewingThe visibility change handler now conditionally checks if
$myPage['Load']is set and reloads the page when necessary, rather than always callingnchanFocusStart(). This provides better control over page reloading behavior for extended viewing sessions.
1325-1325: Modern notification component addedA new web component
<uui-toaster>has been added to replace the legacy jGrowl notification system. This component properly inherits the notification position configuration from the existing settings.api/src/unraid-api/unraid-file-modifier/unraid-file-modifier.service.ts (1)
39-49: Improved modification loading with dynamic importsThe implementation now uses
import.meta.globto dynamically load all modification files, making the codebase more maintainable and extendable. This follows the open/closed principle - new modifications can be added without changing this service.api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php (1)
695-695: Snapshot reflects partner logo replacementThe snapshot correctly reflects the partner logo PHP include replacement, ensuring tests will pass with the new implementation.
api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts (2)
35-45: Well-implemented partner logo injection methodThe
injectPartnerLogomethod correctly handles the replacement of the Unraid logo with a partner logo from the dynamix.my.servers plugin. The method includes proper checks to prevent duplicate replacements.
52-52: Transformation pipeline properly updatedThe
injectPartnerLogomethod has been correctly added to the transformation pipeline in theapplyToSourcemethod.api/src/unraid-api/unraid-file-modifier/modifications/patches/sso.patch (1)
20-21: Re-evaluate the 800-character thresholdHard-coding
strlen($password) > 800risks rejecting valid (shorter) JWTs and accepting junk that simply exceeds the limit. Consider dropping the length gate altogether or using a far smaller sanity check (e.g.> 200) informed by real-world token sizes.api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch (2)
92-93: Verify$notify['position']is always defined
<uui-toaster … position="<?= ($notify['position'] === 'center') ? 'top-center' : $notify['position'] ?>">If
$notifyisn’t in scope at this point, PHP will emit a notice and default tonull, producing an empty attribute. Please confirm the variable is set earlier or guard withisset($notify['position']) ? … : 'top-center'.
70-78: Residual JS may still reference the removed bell iconThe patch removes the
#bellelement and part of its update logic, but other scripts (e.g.openNotifier()) might still touch#bellor.nav-user. Scan the page scripts to ensure all references are gone to prevent runtime errors.api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php (8)
5-10: Improved session management logic.The session initialization code has been refactored with proper formatting and explicit checks, making it more robust when verifying if an existing session is present.
20-54: Improved file handling functions with type hints.The file utility functions have been enhanced with proper PHP return type hints and consistent formatting, making the code more maintainable and providing better IDE support.
64-88: Enhanced cleanupFails function with better formatting.The function has been improved with consistent formatting while maintaining its original logic for cleaning up failed login attempts.
114-116: Simplified credential input fields.The input fields have been simplified to only capture username and password, aligning with the removal of 2FA functionality.
125-135: Simplified login verification flow.The login verification now only checks username and password credentials, with improved error handling and formatting.
427-438: Updated login form markup.The login form has been simplified to remove 2FA-related elements while improving formatting and error message display.
446-459: Added cookie validation checks.New JavaScript code has been added to verify that cookies are enabled in the browser, with a clear error message for users when cookies are disabled.
90-102:Details
❌ Incorrect review comment
Security impact: Two-factor authentication has been removed.
The authentication function has been simplified to only verify username and password, removing the previous two-factor authentication capability. This is a significant change to the security model.
🏁 Script executed:
#!/bin/bash # Check if there are any references to two-factor authentication in the codebase # that might be affected by this change echo "Checking for 2FA references in the codebase..." rg -i "two-?factor|2fa|totp|authenticator" --type php echo "Checking for SSO implementation that might replace 2FA..." rg -i "sso|single sign-?on" --type phpLength of output: 4264
Test fixture only – no impact on production 2FA
The file you’re looking at lives under
modifications/__test__/__fixtures__/downloaded/and represents the raw, downloaded login script used in tests. Removing two-factor logic here does not alter any production authentication code or security model. You can safely ignore this fixture change—production 2FA remains untouched.Likely an incorrect or invalid review comment.
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (9)
31-44: Well-implemented first boot setup check.The method creates or checks for a flag file to determine if first boot setup has already run, with proper error handling and logging.
46-67: Path initialization with proper error handling.The service properly initializes path properties with dynamic imports and includes error checking for critical paths.
103-141: Well-implemented activation data retrieval and validation.The service properly retrieves and validates activation data using class-transformer and class-validator, with comprehensive error handling.
143-176: Comprehensive customization application workflow.The method orchestrates the application of all customizations with proper error handling and logging.
178-201: Partner banner setup with proper file handling.The method correctly copies the partner banner file with appropriate existence checks and error handling.
203-246: Display settings application using Redux store data.The service properly retrieves current settings from the Redux store and applies new settings from activation data.
248-300: Case model configuration with proper error handling.The method properly sets up a custom case model based on activation data, with appropriate file existence checks.
302-351: Server identity updates with proper API integration.The service updates server identity settings and properly triggers system updates using the emcmd utility.
354-407: Generic INI configuration file update helper.A well-implemented helper method for updating INI configuration files, with proper handling of missing files, section creation, and value updates.
api/src/unraid-api/graph/resolvers/customization/activation-code.dto.ts
Outdated
Show resolved
Hide resolved
...d-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (10)
api/src/__test__/store/modules/paths.test.ts (1)
7-8: Trim the redundant key-snapshotKeeping both a full-key snapshot (
Object.keys(paths)=>toMatchSnapshot()) and the explicittoMatchObjectassertion below doubles the maintenance load without adding safety.
If a new key is introduced you will already fail thetoMatchObjectblock (or have to update it intentionally). Consider dropping the first snapshot to reduce future churn.- expect(Object.keys(paths)).toMatchSnapshot();api/src/unraid-api/auth/authentication.guard.ts (1)
74-81: Guard short-circuit is correct but consider typing
getAllAndOverride<boolean>can returnundefined; comparing withtrueis a tad clearer and prevents accidental truthy values:- if (isPublic) { + if (isPublic === true) { return true; }api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (2)
1-8: Remove unused imports
Field,ObjectTypeandGraphQLErrorare imported but never referenced, which will trigger the linter and bloats bundle size.-import { Field, ObjectType, Query, Resolver } from '@nestjs/graphql'; -import { GraphQLError } from 'graphql'; +import { Query, Resolver } from '@nestjs/graphql';
46-71:getPartnerInfo– tighten boolean computation & double I/O
hasPartnerLogois given a path but later treated as a boolean. Rename tologoPath(or cast withBoolean(...)) for clarity.
customizationService.getPartnerLogoWebguiPath()is executed in bothgetActivationDataand here. If the service hits the filesystem each time, you incur two reads per request. Either memoise inside the service (preferred) or reuse the earlier result.Example patch:
- const hasPartnerLogo = await this.customizationService.getPartnerLogoWebguiPath(); // Returns boolean + const logoPath = await this.customizationService.getPartnerLogoWebguiPath();return { hasPartnerLogo: Boolean(logoPath), partnerName: activationData.partnerName, };api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1)
637-642: Snapshot sprawlSnapshot-testing the entire file content after every INI manipulation will quickly balloon the
__snapshots__directory and make diffs noisy. Prefer explicit assertions on the critical keys or usetoMatchInlineSnapshot()with a trimmed expectation.api/src/unraid-api/graph/resolvers/customization/activation-code.dto.ts (2)
9-14: Basic string sanitizationThe sanitization removes potentially dangerous characters (backslashes and quotes) which helps prevent basic injection attacks. For added security, consider using a more comprehensive sanitization library for user inputs.
17-30: Hex color normalization and validationThe function handles both validation and normalization well by adding the # prefix when needed. However, it silently returns an empty string for invalid colors rather than throwing an error or returning null.
Consider making the invalid case handling more explicit:
- return ''; // Return empty string if not a valid hex color after potential modification + return ''; // Return empty string if not a valid hex color (could consider returning null to be more explicit)api/src/unraid-api/graph/resolvers/customization/customization.service.ts (3)
28-28: Remove unnecessary constructorThe empty constructor doesn't add any value and can be removed.
- constructor() {}🧰 Tools
🪛 Biome (1.9.4)
[error] 28-28: This constructor is unnecessary.
Unsafe fix: Remove the unnecessary constructor.
(lint/complexity/noUselessConstructor)
45-99: Comprehensive module initializationThe
onModuleInitmethod handles path initialization, directory checking, and conditional application of customizations with robust error handling.Add path validation for
caseModelCfgandidentCfgsimilar to how you checkconfigFile:this.caseModelCfg = paths.dynamixCaseModelConfig; + if (!this.caseModelCfg) { + this.logger.warn("Could not resolve case model config path from store."); + } this.identCfg = paths.identConfig; + if (!this.identCfg) { + this.logger.warn("Could not resolve identity config path from store."); + }
382-436: Flexible configuration file update utilityThe
updateCfgFilemethod provides a flexible way to update INI-style configuration files, with good error handling and section-level updates.For additional robustness when writing config files, consider creating parent directories if they don't exist:
// Write the updated content back to the file + const dirPath = path.dirname(filePath); + await fs.mkdir(dirPath, { recursive: true }); await fs.writeFile(filePath, newContent + '\n'); // Ensure trailing newline
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
⛔ Files ignored due to path filters (4)
api/dev/activation/assets/case-model.pngis excluded by!**/*.pngapi/dev/activation/assets/logo.svgis excluded by!**/*.svgapi/src/__test__/store/modules/__snapshots__/paths.test.ts.snapis excluded by!**/*.snapapi/src/unraid-api/graph/resolvers/customization/__snapshots__/customization.service.spec.ts.snapis excluded by!**/*.snap
📒 Files selected for processing (21)
.cursor/rules/api-rules.mdc(1 hunks).cursor/rules/default.mdc(1 hunks)api/.env.development(1 hunks)api/.env.test(1 hunks)api/dev/activation/.done(1 hunks)api/dev/activation/activation_code_12345.activationcode(1 hunks)api/generated-schema.graphql(4 hunks)api/src/__test__/store/modules/paths.test.ts(1 hunks)api/src/store/modules/paths.ts(2 hunks)api/src/unraid-api/auth/authentication.guard.ts(4 hunks)api/src/unraid-api/auth/public.decorator.ts(1 hunks)api/src/unraid-api/graph/resolvers/base.model.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/activation-code.dto.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.module.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.service.ts(1 hunks)api/src/unraid-api/graph/resolvers/docker/docker.model.ts(1 hunks)api/src/unraid-api/graph/resolvers/resolvers.module.ts(2 hunks)api/src/unraid-api/unraid-file-modifier/modifications/case-model-copier.modification.ts(1 hunks)api/src/unraid-api/unraid-file-modifier/modifications/partner-logo-copier.modification.ts(1 hunks)
✅ Files skipped from review due to trivial changes (11)
- .cursor/rules/default.mdc
- api/src/unraid-api/graph/resolvers/docker/docker.model.ts
- api/dev/activation/.done
- api/.env.test
- api/src/unraid-api/auth/public.decorator.ts
- api/src/unraid-api/graph/resolvers/resolvers.module.ts
- api/src/unraid-api/graph/resolvers/customization/customization.module.ts
- api/.env.development
- api/dev/activation/activation_code_12345.activationcode
- api/src/unraid-api/graph/resolvers/base.model.ts
- api/src/unraid-api/unraid-file-modifier/modifications/partner-logo-copier.modification.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- .cursor/rules/api-rules.mdc
- api/src/unraid-api/unraid-file-modifier/modifications/case-model-copier.modification.ts
- api/src/store/modules/paths.ts
🧰 Additional context used
🧬 Code Graph Analysis (2)
api/src/unraid-api/auth/authentication.guard.ts (1)
api/src/unraid-api/auth/public.decorator.ts (1)
IS_PUBLIC_KEY(3-3)
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (5)
api/src/unraid-api/graph/directives/use-permissions.directive.ts (3)
UsePermissions(69-85)AuthActionVerb(14-14)AuthPossession(14-14)api/src/unraid-api/auth/public.decorator.ts (1)
Public(4-4)api/src/store/modules/paths.ts (1)
paths(112-116)api/src/store/index.ts (1)
store(6-12)packages/unraid-api-plugin-connect/src/helpers/utils.ts (1)
fileExists(5-8)
🪛 Biome (1.9.4)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts
[error] 28-28: This constructor is unnecessary.
Unsafe fix: Remove the unnecessary constructor.
(lint/complexity/noUselessConstructor)
⏰ Context from checks skipped due to timeout of 90000ms (3)
- GitHub Check: Test API
- GitHub Check: Build Web App
- GitHub Check: Cloudflare Pages
🔇 Additional comments (18)
api/src/__test__/store/modules/paths.test.ts (1)
21-30: UsestringContainingfor every dynamically-resolved dev path
'myservers-keepalive'is asserted against the literal string./dev/Unraid.net/fb_keepalive, while most siblings useexpect.stringContaining(...).
Should the base directory ever change (e.g.,api/devvs./dev), this single hard-coded check will break. Align it with the other flexible assertions:- 'myservers-keepalive': './dev/Unraid.net/fb_keepalive', + 'myservers-keepalive': expect.stringContaining('Unraid.net/fb_keepalive'),api/src/unraid-api/auth/authentication.guard.ts (1)
44-46: Constructor injection looks goodInjecting
Reflectorand callingsuper()is the recommended NestJS pattern; no issues spotted here.api/generated-schema.graphql (4)
759-759: Appropriate addition ofACTIVATION_CODEto Resource enumThe new
ACTIVATION_CODEresource type has been correctly added to the Resource enum, which will enable permission-based access control for activation code features.
1209-1209: Good addition of specification referenceAdding the
@specifiedBydirective to the JSONObject scalar improves documentation by providing a reference to the official ECMA-404 specification.
1392-1424: Well-structured DTOs for activation code featuresThe three new object types (
PublicPartnerInfoDto,ActivationCodeDto, andCustomization) are properly defined with appropriate fields, types, and descriptions. The structure adequately supports the activation code and partner branding features.
1457-1458: Appropriate query endpoints for activation dataThe two new query fields
getActivationDataandpartnerInfoprovide logical access points for retrieving customization data and partner information, respectively.api/src/unraid-api/graph/resolvers/customization/activation-code.dto.ts (4)
7-7: Simple but effective hex color validationThis helper function correctly validates standard hex color formats with the # prefix.
32-40: Clean DTO for partner infoThis DTO is correctly structured with appropriate field types and nullability settings.
42-103: Well-organized ActivationCodeDto with validationThe DTO implements thorough validation and sanitization for all fields. The use of class-validator decorators and transformation functions ensures that data is properly validated and cleansed.
The color fields correctly use the sanitization helper, and the theme/banner fields have appropriate enum validation.
105-129: Well-structured Customization classGood organization with clear field descriptions and appropriate validation. The relative path approach for assets is a clean solution.
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (8)
30-43: Good implementation of boot setup flag mechanismThe method correctly creates and checks for the first boot setup flag, with appropriate logging and error handling.
101-119: Effective activation JSON file detectionThe method properly handles directory access and file finding with good error handling.
121-141: Well-implemented activation data retrievalThe method correctly retrieves, parses, and validates the activation data, with proper error handling and documentation.
159-192: Comprehensive customization application orchestrationThe method cleanly coordinates the application of all customization types with appropriate checks and error handling.
194-218: Partner banner setup with safe file operationsThe method correctly handles file existence checks and copying with appropriate error handling and logging.
220-276: Display settings application with color formattingThe method correctly handles display settings application, including proper color format conversion (removing # from hex colors as required by the config).
278-329: Case model configuration with fallback optionsThe method intelligently handles different sources for the case model, with appropriate file checks and fallback options.
331-380: Server identity application with emcmd integrationThe method correctly updates both the config file and triggers the emcmd utility to apply changes. Good state retrieval from the store and parameter preparation.
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts
Show resolved
Hide resolved
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
♻️ Duplicate comments (3)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (3)
36-46: Mockingstoredoesn't overridestore.getState()The mock implementation for the store doesn't correctly replace the
getState()method, which means the tests could be accessing the real store state instead of the mock paths. This issue was previously identified in a review comment.vi.mock('@app/store/index.js', async () => { const actual = await vi.importActual('@app/store/index.js'); return { ...actual, - getters: { - paths: vi.fn(() => mockPaths), - dynamix: vi.fn(() => mockDynamixState), - emhttp: vi.fn(() => mockEmhttpState), - }, + store: { + getState: () => ({ + paths: mockPaths, + dynamix: mockDynamixState, + emhttp: mockEmhttpState + }), + }, }; });
132-141:should initialize paths from storeis a false positiveThe test succeeds even if
onModuleInit()fails to read from the store because you're manually assigning properties before the test runs.
121-126: 🛠️ Refactor suggestionRemove manual property assignment after service initialization
After fixing the store mock to properly override
getState(), you should be able to remove these manual property assignments. The service should initialize these values from the mocked store duringonModuleInit().// Remove these lines after fixing the store mock - (service as any).activationDir = activationDir; - (service as any).hasRunFirstBootSetup = doneFlag; - (service as any).configFile = userDynamixCfg; - (service as any).caseModelCfg = caseModelCfg; - (service as any).identCfg = identCfg;
🧹 Nitpick comments (7)
web/components/Activation/store/activationCodeData.ts (2)
4-4: Verify import path case consistencyThe import path uses lowercase 'activationcode.query' which is inconsistent with typical camelCase naming conventions. This might cause issues on case-sensitive file systems.
-import { ACTIVATION_CODE_QUERY } from '~/components/Activation/graphql/activationcode.query'; +import { ACTIVATION_CODE_QUERY } from '~/components/Activation/graphql/activationCode.query';
17-18: Remove extra empty lineThere's an unnecessary empty line that can be removed for better code style.
return { activationCode, isFreshInstall, - };web/components/Activation/ActivationModal.vue (1)
14-14: Verify import path case consistencyThe import path uses lowercase 'activationcode.query' which is inconsistent with typical camelCase naming conventions.
-import { PARTNER_INFO_QUERY } from '~/components/Activation/graphql/activationcode.query'; +import { PARTNER_INFO_QUERY } from '~/components/Activation/graphql/activationCode.query';web/components/Activation/WelcomeModal.ce.vue (1)
12-19: Consider moving the GraphQL query to a separate fileThe GraphQL query is defined inline here but imported from a separate file in other components. Consider moving this to a shared query file for consistency.
Consider moving this query to
~/components/Activation/graphql/activationCode.query.tsand importing it like other components do:-const PARTNER_INFO_QUERY = gql` - query PartnerInfo { - partnerInfo { - hasPartnerLogo - partnerName - } - } -`; +import { PARTNER_INFO_QUERY } from '~/components/Activation/graphql/activationCode.query';web/components/Activation/store/activationCodeModal.ts (1)
35-63: Consider moving sequenceIndex inside the store functionThe
sequenceIndexvariable is defined outside the store function, which could potentially cause issues if multiple store instances are created./** * Listen for konami code sequence to close the modal */ const keySequence = [ 'ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a', ]; - let sequenceIndex = 0; + const sequenceIndex = ref(0); const handleKeydown = (event: KeyboardEvent) => { if (event.key === keySequence[sequenceIndex]) { - sequenceIndex++; + sequenceIndex.value++; } else { - sequenceIndex = 0; + sequenceIndex.value = 0; } - if (sequenceIndex === keySequence.length) { + if (sequenceIndex.value === keySequence.length) { setActivationModalHidden(true); window.location.href = '/Tools/Registration'; } };api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1)
57-62: Consider adding comment about authentication dependencyThe resolver method for
partnerInforelies on the parent query's authentication. A comment noting this relationship would help future maintainers understand why there's no explicit@Public()decorator here compared to the standalone query.@ResolveField(() => PublicPartnerInfo, { nullable: true, name: 'partnerInfo' }) // No @Public() decorator here - relies on parent query authentication +// This resolver is accessed through the authenticated `customization` query +// and inherits its authentication context, so no @Public() decorator is needed async resolvePartnerInfo(): Promise<PublicPartnerInfo | null> { return this._getPublicPartnerInfoInternal(); }api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1)
222-243: Implement the skipped test for error handlingThe skipped test for error handling during activation setup should be implemented to ensure proper error handling coverage.
Consider implementing this test by:
- Setting up mocks to simulate a specific error case
- Asserting on the expected error logs and behavior
- Verifying that subsequent steps are still attempted
Remove the
.skiponce implemented.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (18)
api/generated-schema.graphql(4 hunks)api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.service.ts(1 hunks)web/__test__/store/activationCode.test.ts(1 hunks)web/components/Activation/ActivationModal.vue(2 hunks)web/components/Activation/ActivationPartnerLogo.vue(1 hunks)web/components/Activation/ActivationPartnerLogoImg.vue(1 hunks)web/components/Activation/WelcomeModal.ce.vue(4 hunks)web/components/Activation/graphql/activationCode.query.ts(1 hunks)web/components/Activation/store/activationCodeData.ts(1 hunks)web/components/Activation/store/activationCodeModal.ts(1 hunks)web/composables/gql/gql.ts(3 hunks)web/composables/gql/graphql.ts(17 hunks)web/store/activationCode.ts(0 hunks)web/store/server.ts(1 hunks)web/types/server.ts(1 hunks)
💤 Files with no reviewable changes (1)
- web/store/activationCode.ts
✅ Files skipped from review due to trivial changes (2)
- web/types/server.ts
- web/components/Activation/graphql/activationCode.query.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- api/generated-schema.graphql
🧰 Additional context used
🧠 Learnings (1)
web/components/Activation/store/activationCodeData.ts (4)
Learnt from: zackspear
PR: unraid/api#0
File: :0-0
Timestamp: 2025-03-27T23:52:57.888Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files ensures that all web components share a single Pinia store instance, which is the desired behavior. Without this initialization, each web component would have its own isolated store, breaking the intended architecture.
Learnt from: zackspear
PR: unraid/api#0
File: :0-0
Timestamp: 2025-03-27T23:33:13.215Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files would break the build by causing all web components to share a singular Pinia store instance. Each web component needs its own Pinia store instance to function correctly.
Learnt from: zackspear
PR: unraid/api#0
File: :0-0
Timestamp: 2025-03-27T23:33:13.215Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files would break the build by causing all web components to share a singular Pinia store instance. Each web component needs its own Pinia store instance to maintain proper isolation and encapsulation.
Learnt from: zackspear
PR: unraid/api#0
File: :0-0
Timestamp: 2025-03-27T23:52:57.888Z
Learning: In the unraid/api project, Vue components are compiled into web components. The `setActivePinia(createPinia())` call at the module level in store files is intentional and ensures all web components share a single Pinia store instance, which is the desired behavior. This shared state approach is critical for the application's architecture to function correctly.
🧬 Code Graph Analysis (6)
web/components/Activation/store/activationCodeData.ts (2)
web/components/Activation/graphql/activationCode.query.ts (1)
ACTIVATION_CODE_QUERY(12-32)api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1)
activationCode(70-72)
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (5)
web/composables/gql/graphql.ts (4)
Customization(447-454)PublicPartnerInfo(1090-1095)Query(1097-1133)ActivationCode(46-58)api/src/store/index.ts (1)
store(6-12)packages/unraid-api-plugin-connect/src/helpers/utils.ts (1)
fileExists(5-8)api/src/unraid-api/auth/public.decorator.ts (1)
Public(4-4)api/src/unraid-api/graph/directives/use-permissions.directive.ts (3)
UsePermissions(69-85)AuthActionVerb(14-14)AuthPossession(14-14)
web/composables/gql/gql.ts (1)
api/src/graphql/generated/client/gql.ts (1)
graphql(54-56)
web/components/Activation/store/activationCodeModal.ts (3)
web/consts.ts (1)
ACTIVATION_CODE_MODAL_HIDDEN_STORAGE_KEY(1-1)web/components/Activation/store/activationCodeData.ts (1)
useActivationCodeDataStore(9-19)web/store/callbackActions.ts (1)
useCallbackActionsStore(26-184)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (1)
web/composables/gql/graphql.ts (3)
PublicPartnerInfo(1090-1095)ActivationCode(46-58)Customization(447-454)
web/composables/gql/graphql.ts (2)
api/src/unraid-api/graph/directives/use-permissions.directive.ts (2)
AuthActionVerb(14-14)AuthPossession(14-14)web/types/server.ts (1)
Server(70-121)
🪛 Biome (1.9.4)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts
[error] 28-28: This constructor is unnecessary.
Unsafe fix: Remove the unnecessary constructor.
(lint/complexity/noUselessConstructor)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: Cloudflare Pages
🔇 Additional comments (37)
web/components/Activation/ActivationPartnerLogoImg.vue (1)
8-8: Import path updated to match new modular store structure.The import path has been updated to use the new dedicated activation code modal store location, which aligns with the broader architectural changes where activation code functionality has been modularized.
web/store/server.ts (2)
48-48: Import path updated to match new modular store structure.The import path has been updated to use the new dedicated activation code modal store location, which aligns with the broader architectural changes where activation code functionality has been modularized.
1116-1119:Details
❌ Incorrect review comment
Code still references activation code store but not activationCodeData.
The
setServerfunction still attempts to set activation code data using the old pattern, even though according to the AI summary, this data structure has been refactored. Consider whether this activation code data setting logic is still needed or should be updated to use the new store structure.
🏁 Script executed:
#!/bin/bash # Verify if activationCodeData is still being set elsewhere in the codebase rg "activationCodeData" --type ts --type vueLength of output: 72
Let’s rerun the search without restricting file types to ensure we catch any occurrences:
🏁 Script executed:
#!/bin/bash # Search for all references to activationCodeData across the repo rg "activationCodeData" -n .Length of output: 1752
Let’s check where
useActivationCodeStorecomes from and inspect the surrounding code inweb/store/server.ts:
🏁 Script executed:
#!/bin/bash # Locate any definitions or references to useActivationCodeStore rg "useActivationCodeStore" -n . # Show imports and top of server.ts to see where activationCodeStore is coming from sed -n '1,50p' web/store/server.ts # Show context around lines 1110–1130 in server.ts sed -n '1100,1130p' web/store/server.tsLength of output: 3697
Ignore outdated activation code refactor suggestion
The call to
useActivationCodeStore().setData(data.activationCodeData)in
web/store/server.tsis valid:
ActivationCodeDataremains defined inweb/types/server.tsuseActivationCodeStore(fromactivationCodeModal.ts) still exposessetDataand delegates to the underlyingactivationCodeDatastore- tests in
web/__test__/store/activationCode.test.tscover this behaviorNo changes are needed here.
Likely an incorrect or invalid review comment.
web/components/Activation/store/activationCodeData.ts (1)
9-19: The store implements clean reactive access to activation code dataThe store correctly uses Apollo's
useQueryto fetch data and exposes computed properties for activation code and fresh installation status. This is a good approach for making this data reactively available to components.web/components/Activation/ActivationModal.vue (4)
3-3: Good refactoring to use GraphQL instead of store-based dataThe imports have been updated to use Apollo's
useQueryfor data fetching and the new modal store for visibility management, which is a cleaner separation of concerns.Also applies to: 11-14
22-22: Correctly migrated to the new modal storeThe component now uses the dedicated activation code modal store, which follows better separation of concerns.
25-32: Good implementation of reactive partner dataThe component now fetches partner info via GraphQL and handles loading/error states gracefully with fallback values, which is a more robust approach than the previous implementation.
70-70: Template correctly uses computed partner dataThe template updates properly use the computed partner data properties for conditional rendering and prop passing.
Also applies to: 78-79
web/components/Activation/WelcomeModal.ce.vue (3)
21-31: Good error handling for the GraphQL queryThe component properly handles the query execution and includes error logging, which is important for debugging.
33-34: Well-structured computed propertiesThe computed properties correctly extract and provide fallback values for partner data.
98-102: Good UX consideration with disabled buttonDisabling the button while partner info is loading prevents user interaction with incomplete data.
web/components/Activation/store/activationCodeModal.ts (4)
1-11: Review module-level Pinia initializationAs noted in the activationCodeData.ts review, there are conflicting approaches to Pinia initialization in web component contexts. Verify this is the intended approach for your architecture.
Since this is the same pattern used in activationCodeData.ts, the same verification applies.
13-33: Good implementation of modal visibility logicThe store correctly implements the modal visibility logic based on multiple conditions with clear comments explaining the requirements. Using session storage for persistence is appropriate for this use case.
65-71: Good lifecycle management for event listenersThe event listeners are properly added on mount and removed on unmount, following best practices for resource management.
73-77: Clean API with minimal exportsThe store exports only what's needed - the visibility state and a method to update it. This follows good encapsulation principles.
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (4)
18-45: Well-structured resolver with clear separation of concernsThe
CustomizationResolverclass has a clean design, delegating complex operations to theCustomizationService. The empty object return in thecustomization()query with separate field resolvers is a good approach for GraphQL performance, allowing each field to be resolved only when requested.
47-55: Good use of @public() decorator for unauthenticated accessThe
publicPartnerInfoquery properly implements a public endpoint with conditional access based on password presence. This allows for exposing minimal branding information without requiring authentication when appropriate.
64-72: Appropriate permission control for sensitive dataThe
activationCodefield resolver correctly uses@UsePermissionsto restrict access to activation code data based on user permissions. The resource type and action verb are clearly defined.
74-84: Well-designed public access for UI assetsMaking the
caseIconandpartnerLogoresolvers public allows the UI to display branding elements without requiring authentication, which is appropriate for these visual elements.api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (3)
303-321: Good validation test for hex color transformationThis test effectively verifies that invalid hex colors are transformed to empty strings rather than causing validation failures. This helps ensure robustness in handling potentially malformed input data.
323-339: Comprehensive test for hex color normalizationThe test for prepending '#' to valid hex colors ensures the transformer correctly normalizes different input formats. This is important for maintaining consistent color representation in the system.
358-589: Well-organized test suite with thorough edge case coverageThe customization methods tests cover a wide range of scenarios including:
- Asset presence/absence
- Error handling during file operations
- Various combinations of customization data
- Edge cases in hex color handling
This comprehensive coverage helps ensure the service behaves correctly across different inputs and system states.
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (5)
7-14: Good input sanitization implementationThe helper function to sanitize strings removes potentially dangerous characters like backslashes and quotes, which helps prevent injection attacks and ensures data integrity.
16-30: Robust hex color validation and normalizationThe
sanitizeAndValidateHexColorfunction handles multiple formats (with or without '#') and provides graceful fallback to empty string for invalid inputs. This ensures consistent data representation and prevents validation errors.
32-40: PublicPartnerInfo has proper access control designThe
PublicPartnerInfoclass exposes only minimal information (partner name and logo flag) which is appropriate for public access. The@IsBoolean()validation ensures type safety.
42-103: Comprehensive validation for ActivationCode fieldsThe
ActivationCodeclass properly:
- Validates and sanitizes all fields
- Uses
@IsInconstraints for enum-like fields- Provides sensible default for
showBannerGradient- Applies the custom hex color transformation to color fields
This ensures that only valid, sanitized data enters the system.
105-132: Well-designed Customization container classThe
Customizationclass effectively aggregates related data (activation code, partner info, file paths) with clear field descriptions. The@Transformdecorators ensure input sanitization for all string fields.web/composables/gql/gql.ts (4)
16-18: Added PartnerInfo query to Documents typeThis new entry correctly adds the PartnerInfo query to the Documents type, allowing type-safe access to partner information.
44-46: Added PartnerInfo to documents objectThe query document is properly added to the documents object for runtime usage.
87-90: New graphql function overload for PartnerInfo queryThe function overload provides type-safe access to the PartnerInfo query, ensuring proper type checking at compile time.
17-18: Complete and correct integration of ActivationCode queryThe ActivationCode query is properly integrated into:
- The Documents type
- The documents object
- The graphql function overloads
This provides complete type safety for accessing activation code data from the GraphQL API.
Also applies to: 45-46, 91-94
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (5)
28-29: Remove the no-op constructorThe class does not declare any dependencies and the body is empty, so the constructor can be safely removed to satisfy
noUselessConstructorlinting and keep the class lean.
[ suggest_nitpick ]- constructor() {} + + // No constructor needed – Nest will create an instance automatically🧰 Tools
🪛 Biome (1.9.4)
[error] 28-28: This constructor is unnecessary.
Unsafe fix: Remove the unnecessary constructor.
(lint/complexity/noUselessConstructor)
49-60: Guard against missing path values to avoid early-crash scenarios
paths.activationBase(and other path look-ups) may returnundefinedif the store is mis-configured. Attemptingfs.access(undefined)later will throw an opaque ENOENT. Fail fast with a descriptive error (or a sane default) to aid troubleshooting.
[ flag_critical_issue ]- this.activationDir = paths.activationBase; + this.activationDir = paths.activationBase; + if (!this.activationDir) { + throw new Error( + "Store did not provide 'activationBase'. CustomizationService cannot continue." + ); + }
237-246: Drop the redundant dynamicimportofgetters
gettersis already imported at module scope (line 11). Re-importing it on every call adds overhead and shadows the earlier binding. Use the existing reference instead.
[ suggest_optional_refactor ]- const { getters } = await import('@app/store/index.js'); + // `getters` is already in scope – reuse it to avoid an extra dynamic import.
256-261: Preserve explicit “falsey” values forshowBannerGradientThe current truthy check will ignore valid values such as
"0"or"false". Test forundefined/nullinstead so that the caller can explicitly disable the gradient.
[ suggest_essential_refactor ]- if (showBannerGradient) settingsToUpdate['showBannerGradient'] = showBannerGradient; + if (showBannerGradient !== undefined && showBannerGradient !== null) { + settingsToUpdate['showBannerGradient'] = showBannerGradient; + }
343-349: Reuse the existinggettersimport inapplyServerIdentityAs with
applyDisplaySettings, the extra dynamic import is unnecessary. It can also lead to subtle bugs if the two imports diverge (e.g., during hot-reload in tests).
[ suggest_optional_refactor ]- const { getters } = await import('@app/store/index.js'); + // `getters` already imported at module scopeweb/composables/gql/graphql.ts (1)
421-425: Consider enforcing non-nullability for container port numbers
privatePort/publicPortnow use the customPortscalar but remain nullable. If a port is always present when the container exposes it, consider marking the fields as non-nullable to improve type-safety throughout the UI.
[ suggest_nitpick ]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (2)
web/components/Activation/WelcomeModal.ce.vue (2)
40-42: Fix missing DOM selector forconfirmPasswordThe query
window.document.querySelector('#confirmPassword')returnsnull—there's no element withid="confirmPassword"in this component or elsewhere. This will cause runtime errors.Either:
- Add
id="confirmPassword"to the corresponding input element- Update the selector to match an actual field ID
- Use Vue's ref system instead of querySelector for more reliable DOM access:
- const confirmPasswordField = window.document.querySelector('#confirmPassword'); - - if (confirmPasswordField) { + const showFontSizeAdjustment = true; + + if (showFontSizeAdjustment) {
70-71: Ensure ActivationPartnerLogo accepts anamepropThe component is used without a
nameprop here, but in other places it receives one (e.g.,ActivationModal.vue).Either:
- Add the prop to this instance:
<ActivationPartnerLogo :name="partnerInfo?.partnerName" />- Or ensure the component properly handles the absence of a name prop
🧹 Nitpick comments (4)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (3)
4-4: Remove unused import or actually useIsUrl.
IsUrlis imported but never applied, which is a small cleanliness issue and may also signal a missing validation onpartnerUrl. Either drop the import or (preferably) add the decorator to thepartnerUrlfield.-import { IsBoolean, IsIn, IsOptional, IsString, IsUrl } from 'class-validator'; +import { IsBoolean, IsIn, IsOptional, IsString } from 'class-validator';—or—
@IsOptional() - @IsString() + @IsUrl({ require_tld: false }, { message: 'partnerUrl must be a valid URL' })
34-36: Add validation topartnerNamefor consistency.
partnerNamelacks anyclass-validatordecorators, unlike the rest of the scalar fields. Adding@IsOptional()and@IsString()keeps the DTO internally consistent and prevents accidental non-string values from sneaking through.- @Field(() => String, { nullable: true }) - partnerName?: string; + @Field(() => String, { nullable: true }) + @IsOptional() + @IsString() + @Transform(({ value }) => sanitizeString(value)) + partnerName?: string;
17-30: Rethink returning''for invalid hex colours.The transformer coerces invalid values to an empty string, which still satisfies
@IsString()and therefore passes validation. Consider returningundefined(so the property remains truly “unset”) or throwing an error so the client is immediately informed.- return ''; // Return empty string if not a valid hex color after potential modification + return undefined; // Preserve "unset" semantics for invalid coloursThis prevents downstream code from having to filter out falsy-but-defined values.
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1)
6-8: Remove unusedvalidateOrRejectimport.
validateOrRejectis imported but never referenced, adding a tiny bit of noise.-import { plainToInstance } from 'class-transformer'; -import { validateOrReject } from 'class-validator'; +import { plainToInstance } from 'class-transformer';
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
📒 Files selected for processing (13)
api/generated-schema.graphql(4 hunks)api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts(1 hunks)web/__test__/store/activationCode.test.ts(0 hunks)web/components/Activation/ActivationPartnerLogo.vue(1 hunks)web/components/Activation/ActivationPartnerLogoImg.vue(1 hunks)web/components/Activation/WelcomeModal.ce.vue(4 hunks)web/components/Activation/graphql/activationCode.query.ts(1 hunks)web/components/Activation/store/activationCodeData.ts(1 hunks)web/components/Activation/store/activationCodeModal.ts(1 hunks)web/composables/gql/gql.ts(3 hunks)web/composables/gql/graphql.ts(17 hunks)
💤 Files with no reviewable changes (1)
- web/test/store/activationCode.test.ts
✅ Files skipped from review due to trivial changes (1)
- web/components/Activation/ActivationPartnerLogo.vue
🚧 Files skipped from review as they are similar to previous changes (8)
- web/components/Activation/graphql/activationCode.query.ts
- web/components/Activation/store/activationCodeData.ts
- api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts
- web/components/Activation/store/activationCodeModal.ts
- web/components/Activation/ActivationPartnerLogoImg.vue
- api/generated-schema.graphql
- web/composables/gql/gql.ts
- web/composables/gql/graphql.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (1)
web/composables/gql/graphql.ts (3)
PublicPartnerInfo(1088-1096)ActivationCode(46-59)Customization(448-452)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: Cloudflare Pages
🔇 Additional comments (2)
web/components/Activation/WelcomeModal.ce.vue (2)
6-8: Good refactoring to use central activation storeThe imports reflect the architectural shift to use a centralized GraphQL-based activation store, which aligns with the PR objective of moving activation logic into the API.
76-76: Improved UX with loading stateUsing the loading state to disable the button during API operations is a good practice that prevents multiple submissions and provides visual feedback to users.
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
♻️ Duplicate comments (4)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (4)
35-45:⚠️ Potential issueMock for
storedoesn't properly overridegetState()Your current mock implementation for the store adds a
gettersproperty, but doesn't mockstore.getState(), which is what theCustomizationServicelikely calls. This will prevent your tests from properly isolating the service from the real store.vi.mock('@app/store/index.js', async () => { const actual = await vi.importActual('@app/store/index.js'); return { ...actual, - getters: { - paths: vi.fn(() => mockPaths), - dynamix: vi.fn(() => mockDynamixState), - emhttp: vi.fn(() => mockEmhttpState), - }, + store: { + getState: () => ({ + paths: mockPaths, + dynamix: mockDynamixState, + emhttp: mockEmhttpState + }), + }, }; });
16-19:⚠️ Potential issueDuplicate mocks for
emcmdstill presentThe duplicate mock declaration issue from the previous review hasn't been fully addressed. You have two
vi.mock('@app/core/utils/clients/emcmd.js')calls - the first one at line 17 and the second one at line 47. Since Vitest hoists mocks, only the first one (which is an auto-mock) takes effect, making your detailed mock implementation at lines 47-69 ineffective.-vi.mock('@app/core/utils/clients/emcmd.js'); - ...Remove the first simple mock to ensure your detailed implementation is used.
145-154:⚠️ Potential issueTest doesn't properly verify path initialization
This test is a false positive as mentioned in previous review comments. You're manually setting the service's properties in the
beforeEachblock (lines 123-127), then callingonModuleInit()and asserting the values are what you manually set. This doesn't verify that the method actually initializes paths from the store.it('should initialize paths from store', async () => { + // Reset the manually assigned properties + (service as any).activationDir = undefined; + (service as any).configFile = undefined; + (service as any).caseModelCfg = undefined; + (service as any).identCfg = undefined; + await service.onModuleInit(); + expect((service as any).activationDir).toBe(activationDir); expect((service as any).configFile).toBe(userDynamixCfg); expect(loggerLogSpy).toHaveBeenCalledWith( 'CustomizationService initialized with paths from store.' ); });
106-139: 🛠️ Refactor suggestionStop manually assigning service properties in beforeEach
You're manually setting service properties after instantiation in the
beforeEachblock. This creates artificial test conditions and makes tests like "should initialize paths from store" meaningless since you're bypassing the actual initialization logic.beforeEach(async () => { vi.clearAllMocks(); // Clear mocks before each test // Spy on logger methods loggerDebugSpy = vi.spyOn(Logger.prototype, 'debug').mockImplementation(() => {}); loggerLogSpy = vi.spyOn(Logger.prototype, 'log').mockImplementation(() => {}); loggerWarnSpy = vi.spyOn(Logger.prototype, 'warn').mockImplementation(() => {}); loggerErrorSpy = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => {}); const module: TestingModule = await Test.createTestingModule({ providers: [CustomizationService], }).compile(); service = module.get<CustomizationService>(CustomizationService); - // Re-assign paths manually in beforeEach AFTER mocks are cleared and service instantiated - // This simulates the dynamic import within onModuleInit - (service as any).activationDir = activationDir; - (service as any).hasRunFirstBootSetup = doneFlag; - (service as any).configFile = userDynamixCfg; - (service as any).caseModelCfg = caseModelCfg; - (service as any).identCfg = identCfg; // Mock fileExists needed by customization methods vi.mocked(fileExists).mockImplementation(async (p) => { // Assume assets exist by default for these tests unless overridden return ( p === partnerBannerSource || p === bannerAssetPath || p === caseModelAssetPath || p === bannerDestPath ); }); });Instead, fix the store mock to provide these values through the
getState()method and letonModuleInit()set them properly.
🧹 Nitpick comments (3)
web/pages/welcome.vue (1)
35-35: Label doesn't match the displayed value.The label "Manually Hidden" is displaying the
isVisiblevalue, which is confusing since these represent opposite states.-<li>Manually Hidden (`activationModalHidden`): {{ isVisible }}</li> +<li>Manually Hidden (`activationModalHidden`): {{ !isVisible }}</li>api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (2)
856-885: Clean up commented out expectationsThere are several expectations marked with "REMOVED" comments (lines 857-859, 881-884). These should be completely removed rather than left as commented code, as they clutter the file and could cause confusion.
// Other steps should still run expect(loggerLogSpy).toHaveBeenCalledWith('Setting up partner banner...'); expect(loggerLogSpy).toHaveBeenCalledWith('Applying display settings...'); expect(loggerLogSpy).toHaveBeenCalledWith('Applying server identity...'); - // Overall error from applyActivationCustomizations' catch block - // REMOVED: expect(loggerErrorSpy).toHaveBeenCalledWith('Error during activation setup:', readError); - - // NO overall error logged because the writeFile error is caught internally expect(loggerErrorSpy).not.toHaveBeenCalledWith( 'Error during activation setup:', expect.any(Error) ); // This line should remain
65-69: Improve emcmd mock with proper typingsYour
emcmdmock returns a generic object with minimal fields. This is brittle and could lead to TypeScript errors. Consider using a more robust approach with proper type assertions.return { ...actual, - emcmd: vi.fn(async () => mockResponse), // Return the mock response object + emcmd: vi.fn().mockImplementation(async () => mockResponse as import('@app/core/utils/clients/emcmd.js').Response<string>), };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (16)
.cursor/rules/default.mdc(1 hunks)api/dev/Unraid.net/myservers.cfg(1 hunks)api/dev/states/var.ini(1 hunks)api/generated-schema.graphql(4 hunks)api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts(1 hunks)package.json(1 hunks)web/__test__/store/server.test.ts(0 hunks)web/components/Activation/ActivationModal.vue(3 hunks)web/components/Activation/store/activationCodeData.ts(1 hunks)web/components/Activation/store/activationCodeModal.ts(1 hunks)web/components/DummyServerSwitcher.vue(1 hunks)web/helpers/create-apollo-client.ts(2 hunks)web/layouts/default.vue(2 hunks)web/nuxt.config.ts(1 hunks)web/pages/welcome.vue(1 hunks)web/store/server.ts(3 hunks)
💤 Files with no reviewable changes (1)
- web/test/store/server.test.ts
✅ Files skipped from review due to trivial changes (6)
- package.json
- web/nuxt.config.ts
- api/dev/Unraid.net/myservers.cfg
- api/dev/states/var.ini
- web/components/DummyServerSwitcher.vue
- web/layouts/default.vue
🚧 Files skipped from review as they are similar to previous changes (6)
- .cursor/rules/default.mdc
- web/components/Activation/store/activationCodeData.ts
- web/components/Activation/ActivationModal.vue
- web/components/Activation/store/activationCodeModal.ts
- api/generated-schema.graphql
- web/store/server.ts
⏰ Context from checks skipped due to timeout of 90000ms (2)
- GitHub Check: Build API
- GitHub Check: Cloudflare Pages
🔇 Additional comments (9)
web/pages/welcome.vue (3)
3-8: Imports look good and align with PR objectives.These imports properly bring in the necessary activation-related stores and components that support moving activation logic to the API.
12-16: Good store initialization with proper reactivity handling.Using
storeToRefsis the correct approach to maintain reactivity when destructuring properties from Pinia stores.
28-28: Good addition of the ModalsCe component.This integrates well with the overall activation system refactor mentioned in the PR summary.
web/helpers/create-apollo-client.ts (2)
1-1: Import cleanup improves code hygiene.The removal of
ApolloLinkandObservableimports aligns with the elimination of thedisableClientLinkfunctionality, keeping imports to only what's needed.
82-82:Details
✅ Verification successful
Apollo link chain simplified.
The Apollo client link chain has been streamlined by removing the
disableClientLinkfrom the composition. This change aligns with moving activation code logic to the API as stated in the PR objectives.
🏁 Script executed:
#!/bin/bash # Verify that no code in the codebase still relies on the removed disableClientLink rg "disableClientLink" --type tsLength of output: 32
🏁 Script executed:
#!/bin/bash # Re-verify that no code references disableClientLink across all file types rg -n "disableClientLink"Length of output: 25
Link chain simplification verified
No remaining references todisableClientLinkwere found across the codebase. The Apollo link chain removal is safe and aligns with the PR objectives.api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (4)
95-105: LGTM: Well-structured mock activation dataThe mock activation data is well-structured and matches the expected
ActivationCodeinterface. This makes the tests more realistic and improves their value.
337-453: LGTM: Comprehensive tests for getActivationDataThe test suite for the
getActivationDatamethod is well-designed and covers all important scenarios:
- Missing activation directory
- No activation code file
- Directory access errors
- File read errors
- Invalid JSON
- Hex color handling (both with and without # prefix)
- Successfully validating and returning the DTO
This comprehensive approach significantly increases confidence in the code's reliability.
726-941: LGTM: Thorough error handling testsThe separate test suite for
applyActivationCustomizationsspecifically focuses on error handling scenarios, which is excellent practice. The tests verify that:
- The service gracefully handles a missing activation directory
- Errors in one customization step don't prevent other steps from running
- Each method properly logs its specific errors
This approach ensures the service remains resilient even when parts of the customization process fail.
943-1096: LGTM: Excellent unit tests for updateCfgFile utilityThe tests for the
updateCfgFileutility function are particularly well-designed, covering:
- Creating a new file with/without sections
- Merging updates with existing content
- Adding new sections to existing files
- Handling read/write errors
This thorough testing of a utility function demonstrates good attention to detail and will prevent regressions.
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (2)
122-128: Re-assignment of service properties masks initialization issues.Manually reassigning service properties after service instantiation might make tests pass even if the actual initialization in
onModuleInit()is broken. Instead, consider testing the initialization directly by asserting values before and after callingonModuleInit().
155-181: Complex approach for testing missing config path.The test for handling a missing dynamix user config path is unnecessarily complex. Rather than importing the mock module and modifying its return value, consider using the approach from other tests where you directly mock the store's getState method.
🧹 Nitpick comments (3)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (1)
41-45: Consider using IsUrl validator for partner URL.The
partnerUrlfield is currently only validated as a string. If this is intended to be a valid URL, consider adding the@IsUrl()validator to ensure it's properly formatted.@Field(() => String, { nullable: true }) @IsOptional() @IsString() +@IsUrl({ require_tld: false, allow_protocol_relative_urls: true }) @Transform(({ value }) => sanitizeString(value)) partnerUrl?: string;api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)
237-238: Redundant dynamic import of getters.The getters are already imported at the top of the file, making this dynamic import unnecessary. Consider removing it.
-const { getters } = await import('@app/store/index.js');
312-313: Consider implementing Redux store updates.The TODO comment indicates a potential need to dispatch actions to update the Redux store. Consider implementing this feature for immediate UI feedback when display settings change.
Would you like me to provide an implementation for dispatching Redux actions after updating the config file?
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
📒 Files selected for processing (6)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.service.ts(1 hunks)web/_data/serverState.ts(2 hunks)web/store/server.ts(4 hunks)web/types/server.ts(0 hunks)
💤 Files with no reviewable changes (1)
- web/types/server.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- web/store/server.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (1)
web/composables/gql/graphql.ts (3)
PublicPartnerInfo(1088-1096)ActivationCode(46-59)Customization(448-452)
🪛 Biome (1.9.4)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts
[error] 28-28: This constructor is unnecessary.
Unsafe fix: Remove the unnecessary constructor.
(lint/complexity/noUselessConstructor)
⏰ Context from checks skipped due to timeout of 90000ms (3)
- GitHub Check: Build Web App
- GitHub Check: Build API
- GitHub Check: Cloudflare Pages
🔇 Additional comments (13)
web/_data/serverState.ts (3)
9-10: Improved import organization.Moving the Pinia import to its own line at the top of the file follows best practices for import organization.
200-206: Removal of activation code data from server state.The removal of activation code data from the OEM activation object aligns with the PR objective of moving activation code logic into the API. This is a good architectural change that promotes separation of concerns by transferring this data management to dedicated stores with GraphQL queries.
208-212: Improved Pinia store formatting.The formatting changes to the
useDummyServerStoremake the code more consistent and readable while maintaining the same functionality.api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (4)
9-14: Well-implemented string sanitization.The
sanitizeStringfunction effectively removes potentially dangerous characters (backslashes and quotes) from string values, which helps prevent injection attacks.
17-30: Good implementation of hex color validation.The
sanitizeAndValidateHexColorfunction properly:
- Sanitizes input with existing
sanitizeString- Adds a leading # if missing
- Validates the hex color format
- Returns empty string for invalid inputs
This robustly handles different formats of hex color input.
96-122: Comprehensive color handling for header and background.The color fields are properly:
- Nullable in GraphQL
- Optional in validation
- Validated as strings
- Transformed using the custom sanitizeAndValidateHexColor function
The implementation ensures proper hex color format while gracefully handling invalid inputs.
123-128: Good theme constraint using IsIn validator.The
themefield is properly constrained to a set of allowed values using the@IsIn()validator, preventing invalid theme values.api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (3)
404-422: Comprehensive test for hex color prepending.Good test case that verifies the system correctly prepends # to hex colors that are provided without it, ensuring display settings are consistently applied regardless of input format.
599-622: Good test for handling empty color strings.This test ensures that empty strings resulting from invalid hex colors are properly filtered out when applying display settings, preventing errors from propagating through the system.
943-1096: Excellent standalone tests for updateCfgFile utility.These tests comprehensively verify the behavior of the private utility method, covering:
- Creating new files with and without sections
- Merging updates with existing content
- Adding new sections to existing files
- Error handling for both read and write operations
This approach of testing private methods in isolation is valuable for complex utilities.
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (3)
126-157: Well-implemented activation data retrieval with caching.The
getActivationData()method effectively:
- Returns cached data if available
- Locates JSON files with the correct extension
- Parses and validates the data using class-transformer and class-validator
- Gracefully handles errors with appropriate logging
- Only caches valid data
This provides a robust foundation for the activation features.
236-316: Thorough display settings application with edge case handling.The
applyDisplaySettings()method:
- Checks for activation data availability
- Maps properties with custom transformations (removing # from colors)
- Verifies banner file existence before setting banner=image
- Skips empty fields
- Handles errors gracefully
- Provides detailed debug logging
The structured approach makes the code maintainable and robust.
417-472: Well-implemented INI file update utility.The
updateCfgFilehelper method:
- Handles both section-based and root-level updates
- Creates files if they don't exist
- Preserves existing settings not being updated
- Properly stringifies INI format with trailing newline
- Includes comprehensive error handling and logging
This reusable utility provides a solid foundation for all configuration file updates.
api/src/unraid-api/graph/resolvers/customization/customization.service.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (3)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (3)
16-19: Duplicate mocks foremcmdneed to be consolidatedThere are two
vi.mockcalls for the same module. Since Vitest hoists mocks, only the first statement is honored, making the detailed implementation at lines 47-69 ineffective.Apply this diff to fix the issue:
-vi.mock('@app/core/utils/clients/emcmd.js'); - // Mocks vi.mock('fs/promises'); +// Mock emcmd with custom implementation vi.mock('@app/core/utils/clients/emcmd.js');
35-45: Store mock doesn't correctly overridestore.getState()The current mock only adds
gettersbut doesn't replacestore.getState(), which is what the service actually calls. This makes tests potentially unreliable.Apply this diff to properly mock the store:
vi.mock('@app/store/index.js', async () => { const actual = await vi.importActual('@app/store/index.js'); return { ...actual, - getters: { - paths: vi.fn(() => mockPaths), - dynamix: vi.fn(() => mockDynamixState), - emhttp: vi.fn(() => mockEmhttpState), - }, + store: { + getState: () => ({ + paths: mockPaths, + dynamix: mockDynamixState, + emhttp: mockEmhttpState + }), + } }; });
146-161: Test for missing config path is unnecessarily complexThis test modifies mockPaths directly and then restores it, which is fragile and harder to maintain.
Consider simplifying by using a temporary store mock override:
it('should log error if dynamix user config path is missing', async () => { - // Temporarily modify mockPaths to simulate missing user config path - const originalPaths = { ...mockPaths }; - mockPaths['dynamix-config'] = [mockPaths['dynamix-config'][0]]; // Only keep default config - + // Save original mock to restore later + const originalGetState = vi.mocked(store.getState); + + // Mock store to return paths without dynamix-config[1] + vi.mocked(store.getState).mockReturnValueOnce({ + ...originalGetState(), + paths: { + ...mockPaths, + 'dynamix-config': [mockPaths['dynamix-config'][0]], + } + }); await service.onModuleInit(); expect(loggerErrorSpy).toHaveBeenCalledWith( "Could not resolve user dynamix config path (paths['dynamix-config'][1]) from store." ); expect(fs.writeFile).not.toHaveBeenCalledWith(doneFlag, 'true'); - // Restore original paths - mockPaths['dynamix-config'] = originalPaths['dynamix-config']; + // Restore original mock + vi.mocked(store.getState).mockImplementation(originalGetState); });
🧹 Nitpick comments (5)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (5)
193-198: Test could be more specific about.doneflag checkThe test mocks
fileExiststo return true for.done, but the assertion only checks thatfs.readdirwasn't called, which is an indirect verification.Add an explicit log message assertion to verify the skip reason:
it('should skip customizations if .done flag exists', async () => { vi.mocked(fileExists).mockImplementation(async (p) => p === doneFlag); // .done file exists await service.onModuleInit(); expect(fs.readdir).not.toHaveBeenCalled(); // Should not read activation dir for JSON + expect(loggerLogSpy).toHaveBeenCalledWith(expect.stringContaining("First boot setup already complete")); });
209-215: Mock forfs.readFilecould be simplified using a mapThe current implementation uses a series of if-else conditions which becomes harder to maintain as more cases are added.
Use a map to make the mock more maintainable:
vi.mocked(fs.readFile).mockImplementation(async (p) => { - if (p === activationJsonPath) return JSON.stringify(mockActivationData); - if (p === userDynamixCfg) return ini.stringify({}); // Mock empty dynamix cfg - if (p === identCfg) return ini.stringify({}); // Mock empty ident cfg - if (p === caseModelCfg) throw { code: 'ENOENT' }; // Mock case model cfg doesn't exist - throw new Error(`Unexpected readFile: ${p}`); + const mockFiles = { + [activationJsonPath]: JSON.stringify(mockActivationData), + [userDynamixCfg]: ini.stringify({}), + [identCfg]: ini.stringify({}) + }; + + if (p === caseModelCfg) throw { code: 'ENOENT' }; + if (p in mockFiles) return mockFiles[p]; + throw new Error(`Unexpected readFile: ${p}`); });
379-380: Use more specific assertion for error messageUsing
expect.any(SyntaxError)is less specific than checking for the actual error message content.Use a more specific assertion:
expect(loggerErrorSpy).toHaveBeenCalledWith( `Error processing activation file ${activationJsonPath}:`, - expect.any(SyntaxError) + expect.objectContaining({ + message: expect.stringContaining('Unexpected token') + }) );
812-813: Commented out expectation without explanationThere's a commented out expectation without clear explanation why it was removed. This creates confusion about what was intended.
Either add a comment explaining why the expectation was removed or remove the line entirely to avoid confusion.
949-951: Default mock behavior could be improved with common error patternThe default mocking for
fs.readFileonly throws for one specific path, which could lead to unexpected behavior for other paths.Create a consistent default behavior:
vi.mocked(fs.readFile).mockImplementation(async (p) => { if (p === filePath) { const err = new Error('ENOENT') as NodeJS.ErrnoException; err.code = 'ENOENT'; throw err; // Default: file not found for read } - throw new Error(`Unexpected readFile call in updateCfgFile tests: ${p}`); + // Default behavior for any other path + const err = new Error(`Unexpected readFile call: ${p}`) as NodeJS.ErrnoException; + err.code = 'ENOENT'; + throw err; });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
⛔ Files ignored due to path filters (1)
api/src/__test__/store/modules/__snapshots__/paths.test.ts.snapis excluded by!**/*.snap
📒 Files selected for processing (1)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (4)
- GitHub Check: Test API
- GitHub Check: Build Web App
- GitHub Check: Build API
- GitHub Check: Cloudflare Pages
🔇 Additional comments (1)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1)
992-1006: Test forupdateCfgFilewith sections is thorough and well-designedThe test effectively verifies the method correctly merges updates with existing content while preserving other sections and unmodified keys.
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts
Outdated
Show resolved
Hide resolved
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (4)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (4)
238-239: Use the already imported getters instead of reimporting.You're reimporting
gettersfrom '@app/store/index.js' when it's already imported at the top of the file (line 11) and used elsewhere. This duplicate import is unnecessary.-const { getters } = await import('@app/store/index.js'); +// Use the already imported getters from line 11
368-369: Use the already imported getters instead of reimporting.Same issue as in the
applyDisplaySettingsmethod - you're reimportinggetterswhen it's already available.-const { getters } = await import('@app/store/index.js'); +// Use the already imported getters from line 11
186-194: Consider removing redundant directory check.This directory check is redundant since it's already performed in
onModuleInit, as acknowledged in your comment. Consider removing it to simplify the code.-// Check if activation dir exists (redundant if onModuleInit succeeded, but safe) -try { - await fs.access(this.activationDir); -} catch (dirError: any) { - if (dirError.code === 'ENOENT') { - this.logger.warn('Activation directory disappeared after init? Skipping.'); - return; - } - throw dirError; // Rethrow other errors -}
419-473: Consider adding file locking mechanism for config updates.The
updateCfgFilemethod directly modifies configuration files without any locking mechanism. This could lead to race conditions if multiple processes are modifying these files concurrently. Consider implementing a simple file locking strategy.You could use a package like
proper-lockfileto implement file locking:import * as lockfile from 'proper-lockfile'; private async updateCfgFile( filePath: string, section: string | null, updates: Record<string, string> ) { let configData: any = {}; let release; try { // Acquire a lock on the file release = await lockfile.lock(filePath, { retries: 3 }); try { const content = await fs.readFile(filePath, 'utf-8'); configData = ini.parse(content); } catch (error: any) { // Handle errors as before } // Update config as before // Write changes const newContent = ini.stringify(configData); await fs.writeFile(filePath, newContent + '\n'); this.logger.log(`Config file ${filePath} updated successfully.`); } catch (error) { this.logger.error(`Error updating config file ${filePath}:`, error); throw error; } finally { // Release the lock if it was acquired if (release) await release(); } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
📒 Files selected for processing (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts(1 hunks)web/components/Activation/WelcomeModal.ce.vue(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- web/components/Activation/WelcomeModal.ce.vue
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: Cloudflare Pages
🔇 Additional comments (3)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (3)
1-15: The overall architecture looks good.The service is well-structured with clear responsibilities for managing system customizations based on activation data. The code is organized logically with appropriate use of NestJS patterns and dependency management.
121-152: Great implementation of the activation data retrieval.The caching strategy and error handling in this method are well implemented. The use of class-transformer and class-validator ensures that the activation data is properly validated before use.
251-285: Good use of typed mappings for configuration transformation.The type-safe approach to mapping activation data properties to configuration keys is well-designed. The transformation functions and optional value handling make the code robust and maintainable.
api/src/unraid-api/graph/resolvers/customization/customization.service.ts
Outdated
Show resolved
Hide resolved
api/src/unraid-api/graph/resolvers/customization/customization.service.ts
Outdated
Show resolved
Hide resolved
api/src/unraid-api/graph/resolvers/customization/customization.service.ts
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts (1)
32-42: Added method to remove default Unraid logo from page layout.This method removes the default Unraid logo anchor tag by replacing it with an empty string, making room for partner branding to be injected elsewhere. The implementation includes a check to ensure the replacement only happens when necessary.
There appears to be unnecessary whitespace in the
replaceStringdeclaration. Consider removing the extra newlines and whitespace:- const replaceString = - ''; + const replaceString = '';🧰 Tools
🪛 GitHub Check: Test API
[failure] 35-35:
Delete⏎···········🪛 GitHub Check: Build API
[failure] 35-35:
Delete⏎···········
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
⛔ Files ignored due to path filters (1)
api/dev/webGui/images/UN-logotype-gradient.svgis excluded by!**/*.svg
📒 Files selected for processing (15)
api/generated-schema.graphql(4 hunks)api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php(1 hunks)api/src/unraid-api/unraid-file-modifier/modifications/case-model-copier.modification.ts(1 hunks)api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts(1 hunks)api/src/unraid-api/unraid-file-modifier/modifications/partner-logo-copier.modification.ts(1 hunks)api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch(4 hunks)api/src/unraid-api/unraid-file-modifier/unraid-file-modifier.service.ts(1 hunks)plugin/plugins/dynamix.unraid.net.plg(0 hunks)plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/activation-code-extractor.php(0 hunks)plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/partner-logo.php(0 hunks)plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php(0 hunks)web/components/Activation/ActivationModal.vue(2 hunks)web/components/HeaderOsVersion.ce.vue(2 hunks)web/nuxt.config.ts(3 hunks)web/pages/index.vue(2 hunks)
💤 Files with no reviewable changes (4)
- plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/partner-logo.php
- plugin/plugins/dynamix.unraid.net.plg
- plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/activation-code-extractor.php
- plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php
✅ Files skipped from review due to trivial changes (1)
- api/src/unraid-api/unraid-file-modifier/modifications/case-model-copier.modification.ts
🚧 Files skipped from review as they are similar to previous changes (7)
- api/src/unraid-api/unraid-file-modifier/unraid-file-modifier.service.ts
- web/components/Activation/ActivationModal.vue
- web/nuxt.config.ts
- api/src/unraid-api/unraid-file-modifier/modifications/test/snapshots/DefaultPageLayout.php.modified.snapshot.php
- api/src/unraid-api/unraid-file-modifier/modifications/partner-logo-copier.modification.ts
- api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch
- api/generated-schema.graphql
🧰 Additional context used
🪛 GitHub Check: Test API
api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts
[failure] 35-35:
Delete ⏎···········
🪛 GitHub Check: Build API
api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts
[failure] 35-35:
Delete ⏎···········
⏰ Context from checks skipped due to timeout of 90000ms (2)
- GitHub Check: Build Web App
- GitHub Check: Cloudflare Pages
🔇 Additional comments (8)
web/pages/index.vue (2)
3-3: Logo component import removed to support the dynamic partner branding.The removal of
BrandLogofrom imports aligns with the broader changes moving to dynamic partner branding handled via the new activation code system.
114-116: Logo element removed from header.The static logo and link have been removed, which corresponds with the import changes. The container class was also updated to remove horizontal padding (
px-4). This change streamlines the UI to support dynamic partner branding implemented elsewhere.web/components/HeaderOsVersion.ce.vue (5)
12-12: Added import for the new activation code data store.This import adds support for dynamic partner information through the activation code system.
20-20: Added access to partner information from the new store.The
partnerInforeactive reference is destructured from the new activation code data store.
25-30: Added computed property for partner URL.Good implementation of the
logoUrlcomputed property with proper fallback to the default Unraid URL when partner URL is not available.
73-76: Added dynamic logo link in the header.The logo now links to either the partner URL or the default Unraid URL based on the activation code data. The image source still points to the Unraid logo image with responsive width classes.
77-115: Restructured OS version and update status badges.The existing OS version badge and update status controls are now contained within a flex container below the logo. The functionality remains unchanged, but the visual organization is improved.
api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts (1)
49-49: Added logo injection step to transformation pipeline.The
injectPartnerLogomethod has been incorporated into the transformation pipeline inapplyToSource.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)
44-52: 🛠️ Refactor suggestionProvide a default value for the configFile path.
The error handling for the missing configFile path could be improved. Currently, initialization simply stops if the path is missing, which may lead to unpredictable behavior later.
Consider adding a default path instead of returning early:
this.configFile = paths['dynamix-config']?.[1]; if (!this.configFile) { this.logger.error( "Could not resolve user dynamix config path (paths['dynamix-config'][1]) from store." ); - // Handle error appropriately - maybe throw, or use a default? - // For now, we'll let subsequent operations fail if configFile is needed. - return; // Stop initialization if critical path is missing + this.configFile = '/etc/local/emhttp/dynamix.cfg'; // Use a sensible default + this.logger.warn(`Using default config path: ${this.configFile}`); }
310-310: 🛠️ Refactor suggestionAddress the TODO comment about Redux store updates.
This TODO comment suggests that the state update might not be immediately reflected in the Redux store.
- // @todo: Consider dispatching an action to update Redux store if needed immediately + // Update Redux store to reflect the changed display settings + try { + // Import actions and dispatch to ensure store is updated + const { actions } = await import('@app/store/index.js'); + actions.dynamix.update({ display: settingsToUpdate }); + this.logger.log('Redux store updated with new display settings.'); + } catch (storeError) { + this.logger.warn('Failed to update Redux store:', storeError); + // Non-fatal error, continue + }
🧹 Nitpick comments (4)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (4)
83-84: Improve error handling specificity.The comment mentions a specific error case but the code checks for two conditions.
- if (error.code === 'ENOENT' && error.path === this.activationDir) { + // Check specifically for activation directory not found errors + if (error.code === 'ENOENT' && error.path === this.activationDir) {
214-231: Simplify error handling in setupPartnerBanner.The nested try-catch structure is overly complex since the file existence check doesn't throw errors.
private async setupPartnerBanner() { this.logger.log('Setting up partner banner...'); const paths = getters.paths(); const partnerBannerSource = paths.partnerBannerSource; const partnerBannerTarget = paths.partnerBannerTarget; - try { - // Always overwrite if partner banner exists - if (await fileExists(partnerBannerSource)) { - this.logger.log(`Partner banner found at ${partnerBannerSource}, overwriting original.`); - try { - await fs.copyFile(partnerBannerSource, partnerBannerTarget); - this.logger.log('Partner banner copied over the original banner.'); - } catch (copyError: any) { - this.logger.warn( - `Failed to replace the original banner with the partner banner: ${copyError.message}` - ); - } - } else { - this.logger.log('Partner banner file not found, skipping banner setup.'); - } - } catch (error) { - this.logger.error('Error setting up partner banner:', error); - } + // Always overwrite if partner banner exists + if (await fileExists(partnerBannerSource)) { + this.logger.log(`Partner banner found at ${partnerBannerSource}, overwriting original.`); + try { + await fs.copyFile(partnerBannerSource, partnerBannerTarget); + this.logger.log('Partner banner copied over the original banner.'); + } catch (copyError: any) { + this.logger.warn( + `Failed to replace the original banner with the partner banner: ${copyError.message}` + ); + } + } else { + this.logger.log('Partner banner file not found, skipping banner setup.'); + } }
338-358: Handle case when no custom model exists more explicitly.The current implementation skips writing to the config file if no custom model is found, but the logic could be more explicit.
Consider making this behavior more explicit:
let modelToSet: string | null = null; // Check if the custom image file exists in assets if (await fileExists(caseModelSource)) { // Use the *target* filename which CaseModelCopierModification will create modelToSet = path.basename(paths.caseModelTarget); // e.g., 'case-model.png' this.logger.log('Custom case model file found in assets, config will be set.'); } else { this.logger.log('No custom case model file found in assets.'); + // Explicitly note that we're keeping the existing model (if any) + this.logger.log('Keeping existing case model configuration.'); } // If a model was determined, write it to the config file if (modelToSet) { try { await fs.writeFile(this.caseModelCfg, modelToSet); this.logger.log(`Case model set to ${modelToSet} in ${this.caseModelCfg}`); } catch (writeError: any) { // Added type annotation this.logger.error(`Failed to write case model config: ${writeError.message}`); } }
400-417: Separate error handling for external operations.The method calls several external operations but handles all errors in a single try-catch block. It would be better to handle each operation's errors separately.
Consider separating the error handling for each external operation:
try { // Update ident.cfg first await this.updateCfgFile(this.identCfg, null, paramsToUpdate); this.logger.log(`Server identity updated in ${this.identCfg}`); + try { // Trigger emhttp update via emcmd const updateParams = { ...paramsToUpdate, changeNames: 'Apply' }; this.logger.log(`Calling emcmd with params:`, updateParams); await emcmd(updateParams); this.logger.log('emcmd executed successfully.'); + } catch (emcmdError) { + this.logger.error('Error executing emcmd:', emcmdError); + // Continue with other operations + } + try { // Reload nginx and update DNS await store.dispatch(reloadNginxAndUpdateDNS()); this.logger.log('Nginx reloaded and DNS updated successfully.'); + } catch (reloadError) { + this.logger.error('Error reloading nginx and updating DNS:', reloadError); + } } catch (error) { this.logger.error('Error applying server identity:', error); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
📒 Files selected for processing (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts(1 hunks)web/components/Activation/WelcomeModal.ce.vue(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- web/components/Activation/WelcomeModal.ce.vue
⏰ Context from checks skipped due to timeout of 90000ms (2)
- GitHub Check: Build and Deploy Plugin / Build and Deploy Plugin
- GitHub Check: Cloudflare Pages
🔇 Additional comments (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)
119-123: LGTM! Good use of caching for activation data.The caching implementation for activation data is well-executed and will help performance.
419-473: Well-implemented INI file updating function.The
updateCfgFilemethod is well-structured and includes thorough error handling for file operations. The comments explaining the INI library's behavior are particularly helpful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)
44-52: 🛠️ Refactor suggestionProvide a default value for missing configFile path.
The code correctly logs an error when the configFile path is missing, but still proceeds with initialization. It would be better to either throw an error or use a default path to prevent unexpected failures in downstream operations.
this.configFile = paths['dynamix-config']?.[1]; if (!this.configFile) { this.logger.error( "Could not resolve user dynamix config path (paths['dynamix-config'][1]) from store." ); - // Handle error appropriately - maybe throw, or use a default? - // For now, we'll let subsequent operations fail if configFile is needed. - return; // Stop initialization if critical path is missing + this.configFile = '/etc/local/emhttp/dynamix.cfg'; // Use a sensible default + this.logger.warn(`Using default config path: ${this.configFile}`); }
323-323: 🛠️ Refactor suggestionAddress the TODO comment about Redux store updates.
This TODO comment suggests that the state update might not be immediately reflected in the Redux store.
- // @todo: Consider dispatching an action to update Redux store if needed immediately + // Update Redux store to reflect the changed display settings + try { + const { actions } = await import('@app/store/index.js'); + store.dispatch(actions.dynamix.updateDisplay(settingsToUpdate)); + this.logger.log('Redux store updated with new display settings.'); + } catch (storeError) { + this.logger.warn('Failed to update Redux store:', storeError); + // Non-fatal error, continue + }
🧹 Nitpick comments (3)
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1)
38-42: Consider caching the password check result.For better performance, consider caching the result of this check since the password status rarely changes during runtime.
private async isPasswordSet(): Promise<boolean> { + // Use static variable to cache result + if (this.passwordCheckResult !== undefined) return this.passwordCheckResult; const paths = store.getState().paths; const hasPasswd = await fileExists(paths.passwd); + this.passwordCheckResult = hasPasswd; return hasPasswd; }api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)
27-36: Use better flag creation strategy.The current implementation might face race conditions if called concurrently. Consider using a more atomic approach.
async createOrGetFirstBootSetupFlag(): Promise<boolean> { await fs.mkdir(this.activationDir, { recursive: true }); - if (await fileExists(this.hasRunFirstBootSetup)) { - this.logger.log('First boot setup flag file already exists.'); - return true; // Indicate setup was already done based on flag presence - } - await fs.writeFile(this.hasRunFirstBootSetup, 'true'); - this.logger.log('First boot setup flag file created.'); - return false; // Indicate setup was just marked as done + try { + // Try to create file with exclusive flag + await fs.writeFile(this.hasRunFirstBootSetup, 'true', { flag: 'wx' }); + this.logger.log('First boot setup flag file created.'); + return false; // Indicate setup was just marked as done + } catch (error: any) { + if (error.code === 'EEXIST') { + this.logger.log('First boot setup flag file already exists.'); + return true; // Indicate setup was already done based on flag presence + } + throw error; // Re-throw unexpected errors + } }
173-184: Consider using a more efficient approach for base64 conversion.For large logo files, reading the entire file into memory as base64 could be memory intensive.
public async getPartnerLogoRaw(): Promise<string | null> { const path = getters.paths().partnerLogoTarget; if (await fileExists(path)) { - const fileContent = await fs.readFile(path, 'base64'); - return fileContent; + // Use stream-based approach for more efficient memory usage + return new Promise((resolve, reject) => { + try { + const readStream = createReadStream(path); + const chunks: Buffer[] = []; + readStream.on('data', (chunk) => chunks.push(Buffer.from(chunk))); + readStream.on('error', (err) => reject(err)); + readStream.on('end', () => { + const buffer = Buffer.concat(chunks); + resolve(buffer.toString('base64')); + }); + } catch (error) { + reject(error); + } + }); } return null; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
📒 Files selected for processing (10)
api/generated-schema.graphql(4 hunks)api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.service.ts(1 hunks)web/components/Activation/ActivationPartnerLogoImg.vue(1 hunks)web/components/Activation/WelcomeModal.ce.vue(3 hunks)web/components/Activation/graphql/activationCode.query.ts(1 hunks)web/composables/gql/gql.ts(3 hunks)web/composables/gql/graphql.ts(17 hunks)web/store/theme.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
- web/components/Activation/ActivationPartnerLogoImg.vue
- web/components/Activation/WelcomeModal.ce.vue
- web/components/Activation/graphql/activationCode.query.ts
- api/generated-schema.graphql
- web/composables/gql/gql.ts
🧰 Additional context used
🧬 Code Graph Analysis (2)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (1)
web/composables/gql/graphql.ts (3)
PublicPartnerInfo(1089-1099)ActivationCode(46-59)Customization(448-452)
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (5)
web/composables/gql/graphql.ts (4)
Customization(448-452)PublicPartnerInfo(1089-1099)Query(1101-1138)ActivationCode(46-59)api/src/store/index.ts (1)
store(6-12)packages/unraid-api-plugin-connect/src/helpers/utils.ts (1)
fileExists(5-8)api/src/unraid-api/graph/directives/use-permissions.directive.ts (3)
UsePermissions(69-85)AuthActionVerb(14-14)AuthPossession(14-14)api/src/unraid-api/auth/public.decorator.ts (1)
Public(4-4)
🪛 GitHub Check: Test API
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts
[failure] 34-34:
Replace await·this.customizationService.getPartnerLogoRaw( with (await·this.customizationService.getPartnerLogoRaw()
🪛 GitHub Check: Build API
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts
[failure] 34-34:
Replace await·this.customizationService.getPartnerLogoRaw( with (await·this.customizationService.getPartnerLogoRaw()
⏰ Context from checks skipped due to timeout of 90000ms (2)
- GitHub Check: Build Web App
- GitHub Check: Cloudflare Pages
🔇 Additional comments (18)
web/store/theme.ts (1)
127-127: Good addition - exposes the setCssVars function for external use.This change makes the
setCssVarsfunction accessible outside the store, which supports the new activation code-driven theming features. The function was previously only used internally (triggered by the watcher on line 115), but now external components can trigger CSS variable updates directly when needed.api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (4)
25-36: LGTM! Well-structured method for partner info retrieval.The method efficiently combines partner logo existence check and activation data into a single PublicPartnerInfo object.
🧰 Tools
🪛 GitHub Check: Test API
[failure] 34-34:
Replaceawait·this.customizationService.getPartnerLogoRaw(with(await·this.customizationService.getPartnerLogoRaw()🪛 GitHub Check: Build API
[failure] 34-34:
Replaceawait·this.customizationService.getPartnerLogoRaw(with(await·this.customizationService.getPartnerLogoRaw()
51-54: Good implementation of authenticated query.The resolver properly returns an empty object and relies on field resolvers for population, which follows GraphQL best practices.
56-64: Well-designed public endpoint with security check.Good use of the @public() decorator combined with password check to conditionally expose partner info only when no password is set.
66-79: Appropriate authorization handling for different fields.The implementation correctly applies permissions to the sensitive activationCode field while leaving partnerInfo accessible, matching the security requirements.
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (5)
7-14: Good security practice with string sanitization.The sanitizeString function properly removes potential injection vectors by filtering out quotes and backslashes.
17-30: Well-implemented hex color validation and normalization.The function handles different color formats well, ensuring consistent output with proper validation.
32-66: LGTM! Robust model design with proper validation.The PublicPartnerInfo class uses appropriate decorators for validation and transformation, ensuring data safety.
124-131: Nice handling of boolean transformation.Good approach for handling boolean values that might come in different formats (boolean or string).
133-137: Good use of enum validation.The @isin decorator ensures the theme value is restricted to valid options.
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)
58-91: Well-structured initialization logic.Good pattern for handling potential directory existence issues and appropriately applying customizations only on first boot.
377-430: LGTM! Thorough implementation of server identity updates.The method handles all necessary updates including configuration files, emhttp updates, and system services.
web/composables/gql/graphql.ts (6)
25-26: Good addition of Port scalar type.Adding a specific scalar for TCP ports improves type safety and documentation.
46-59: Well-structured ActivationCode type.The type includes all necessary fields for customization with appropriate nullability.
279-292: Good practice using enums for authorization verbs and possession.Using enums provides better type safety and documentation than string literals.
1043-1061: Improved organization with ParityCheckMutations.Grouping related mutations into a dedicated type improves API organization and maintainability.
1089-1099: Well-documented PublicPartnerInfo type.Good use of field descriptions to document the purpose and format of each field.
1720-1728: LGTM! Well-structured GraphQL queries.The queries properly fetch the data needed for the activation code features.
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts
Outdated
Show resolved
Hide resolved
api/src/unraid-api/graph/resolvers/customization/customization.service.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)
45-53: 🛠️ Refactor suggestionProvide a default value for missing configFile path.
The code logs an error when configFile path is missing but then returns without providing a fallback. This could lead to runtime errors when the path is used later.
this.configFile = paths['dynamix-config']?.[1]; if (!this.configFile) { this.logger.error( "Could not resolve user dynamix config path (paths['dynamix-config'][1]) from store." ); - // Handle error appropriately - maybe throw, or use a default? - // For now, we'll let subsequent operations fail if configFile is needed. - return; // Stop initialization if critical path is missing + this.configFile = '/boot/config/plugins/dynamix/dynamix.cfg'; // Use default path + this.logger.warn(`Using default config path: ${this.configFile}`); }
305-307: 🛠️ Refactor suggestionAddress the TODO comment about Redux store updates.
The TODO comment indicates the Redux store might need to be updated after applying display settings.
await this.updateCfgFile(this.configFile, 'display', settingsToUpdate); this.logger.log('Display settings updated in config file.'); - // @todo: Consider dispatching an action to update Redux store if needed immediately + // Update Redux store to reflect the changed display settings + try { + const { actions } = await import('@app/store/index.js'); + actions.dynamix.update({ display: settingsToUpdate }); + this.logger.log('Redux store updated with new display settings.'); + } catch (storeError) { + this.logger.warn('Failed to update Redux store:', storeError); + // Non-fatal error, continue with customization + }
🧹 Nitpick comments (7)
api/src/utils.ts (2)
281-292: Consider adding input validation for path conversion.The function handles standard cases correctly, but lacks validation for edge cases like empty strings, paths not starting with the expected prefix, or paths containing unexpected characters.
export const convertWebGuiPathToAssetPath = (webGuiPath: string): string => { + // Validate input + if (!webGuiPath || typeof webGuiPath !== 'string') { + return ''; + } + // Ensure path has expected prefix + if (!webGuiPath.startsWith('/usr/local/emhttp/')) { + return webGuiPath; // Return unchanged if not matching expected pattern + } // Strip the leading /usr/local/emhttp/ from the path const assetPath = webGuiPath.replace('/usr/local/emhttp/', '/'); return assetPath; };🧰 Tools
🪛 GitHub Check: Build API
[failure] 292-292:
Insert⏎
[failure] 292-292:
Newline required at end of file but not found🪛 GitHub Check: Test API
[failure] 292-292:
Insert⏎
[failure] 292-292:
Newline required at end of file but not found
4-16: Fix linting issues for code style consistency.There are several linting errors flagged by the static analysis:
- Extra blank lines at various places in the file
- Missing trailing newline at the end of the file
// Remove extra blank lines - - import strftime from 'strftime'; - - // Fix missing trailing newline at the end of the file return assetPath; }; +Also applies to: 292-292
🧰 Tools
🪛 GitHub Check: Build API
[failure] 4-4:
Replace⏎⏎import·strftime·from·'strftime';⏎⏎withimport·strftime·from·'strftime';
[failure] 4-4:
More than 1 blank line not allowed
[failure] 8-8:
More than 1 blank line not allowed
[failure] 11-11:
Delete⏎⏎⏎⏎
[failure] 13-13:
More than 1 blank line not allowed🪛 GitHub Check: Test API
[failure] 4-4:
Replace⏎⏎import·strftime·from·'strftime';⏎⏎withimport·strftime·from·'strftime';
[failure] 4-4:
More than 1 blank line not allowed
[failure] 8-8:
More than 1 blank line not allowed
[failure] 11-11:
Delete⏎⏎⏎⏎
[failure] 13-13:
More than 1 blank line not allowedapi/src/unraid-api/graph/resolvers/customization/customization.service.ts (4)
152-152: Remove extra blank line.There's an extra blank line that's triggering linting errors.
} } - public async getCaseIconWebguiPath(): Promise<string | null> {🧰 Tools
🪛 GitHub Check: Build API
[failure] 152-152:
Delete⏎
[failure] 152-152:
More than 1 blank line not allowed🪛 GitHub Check: Test API
[failure] 152-152:
Delete⏎
[failure] 152-152:
More than 1 blank line not allowed
153-159: Add input validation for path resolution method.The method returns null if the file doesn't exist, but should also handle potential issues with the path itself.
public async getCaseIconWebguiPath(): Promise<string | null> { const paths = getters.paths(); + if (!paths || !paths.caseModelSource || !paths.caseModelTarget) { + this.logger.warn('Missing required case model paths'); + return null; + } if (await fileExists(paths.caseModelSource)) { return convertWebGuiPathToAssetPath(paths.caseModelTarget); } return null; }
161-167: Add input validation for partner logo path resolution.Similar to the case icon method, this should handle missing paths.
public async getPartnerLogoWebguiPath(): Promise<string | null> { const paths = getters.paths(); + if (!paths || !paths.partnerLogoSource || !paths.partnerLogoTarget) { + this.logger.warn('Missing required partner logo paths'); + return null; + } if (await fileExists(paths.partnerLogoSource)) { return convertWebGuiPathToAssetPath(paths.partnerLogoTarget); } return null; }
416-426: Improve error handling in updateCfgFile method.The method handles file not found errors but should be more explicit about file permissions or other I/O errors.
private async updateCfgFile( filePath: string, section: string | null, updates: Record<string, string> ) { let configData: any = {}; // Use 'any' for flexibility with ini structure try { const content = await fs.readFile(filePath, 'utf-8'); // Parse the INI file content. Note: ini library parses values as strings by default. // It might interpret numbers/booleans if not quoted, but our values are always quoted. configData = ini.parse(content); } catch (error: any) { if (error.code === 'ENOENT') { this.logger.log(`Config file ${filePath} not found, will create it.`); // Initialize configData as an empty object if file doesn't exist + } else if (error.code === 'EACCES') { + this.logger.error(`Permission denied reading config file ${filePath}`); + throw new Error(`Permission denied reading config file ${filePath}: ${error.message}`); } else { this.logger.error(`Error reading config file ${filePath}:`, error); throw error; // Re-throw other errors } }web/composables/gql/graphql.ts (1)
1691-1692: Clarify the difference between domain and domains.The
Vmstype has bothdomainanddomainsfields, which could cause confusion. Consider documenting the difference between these fields or consolidating them if they serve the same purpose.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
📒 Files selected for processing (12)
api/generated-schema.graphql(4 hunks)api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.service.ts(1 hunks)api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/auth-request.php.modified.snapshot.php(1 hunks)api/src/unraid-api/unraid-file-modifier/modifications/auth-request.modification.ts(2 hunks)api/src/unraid-api/unraid-file-modifier/modifications/patches/auth-request.patch(1 hunks)api/src/utils.ts(2 hunks)web/components/Activation/ActivationPartnerLogoImg.vue(1 hunks)web/components/Activation/graphql/activationCode.query.ts(1 hunks)web/composables/gql/gql.ts(3 hunks)web/composables/gql/graphql.ts(17 hunks)
✅ Files skipped from review due to trivial changes (1)
- api/src/unraid-api/unraid-file-modifier/modifications/patches/auth-request.patch
🚧 Files skipped from review as they are similar to previous changes (6)
- web/components/Activation/ActivationPartnerLogoImg.vue
- api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts
- web/components/Activation/graphql/activationCode.query.ts
- api/generated-schema.graphql
- web/composables/gql/gql.ts
- api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
api/src/unraid-api/unraid-file-modifier/modifications/auth-request.modification.ts (2)
api/src/utils.ts (1)
convertWebGuiPathToAssetPath(288-292)api/src/store/index.ts (1)
getters(18-29)
🪛 GitHub Check: Build API
api/src/unraid-api/graph/resolvers/customization/customization.service.ts
[failure] 152-152:
Delete ⏎
[failure] 152-152:
More than 1 blank line not allowed
api/src/utils.ts
[failure] 4-4:
Replace ⏎⏎import·strftime·from·'strftime';⏎⏎ with import·strftime·from·'strftime';
[failure] 4-4:
More than 1 blank line not allowed
[failure] 8-8:
More than 1 blank line not allowed
[failure] 11-11:
Delete ⏎⏎⏎⏎
[failure] 13-13:
More than 1 blank line not allowed
[failure] 292-292:
Insert ⏎
[failure] 292-292:
Newline required at end of file but not found
🪛 GitHub Check: Test API
api/src/unraid-api/graph/resolvers/customization/customization.service.ts
[failure] 152-152:
Delete ⏎
[failure] 152-152:
More than 1 blank line not allowed
api/src/utils.ts
[failure] 4-4:
Replace ⏎⏎import·strftime·from·'strftime';⏎⏎ with import·strftime·from·'strftime';
[failure] 4-4:
More than 1 blank line not allowed
[failure] 8-8:
More than 1 blank line not allowed
[failure] 11-11:
Delete ⏎⏎⏎⏎
[failure] 13-13:
More than 1 blank line not allowed
[failure] 292-292:
Insert ⏎
[failure] 292-292:
Newline required at end of file but not found
⏰ Context from checks skipped due to timeout of 90000ms (2)
- GitHub Check: Build Web App
- GitHub Check: Cloudflare Pages
🔇 Additional comments (14)
api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/auth-request.php.modified.snapshot.php (1)
18-18: LGTM! Updated whitelist entry matches expected changes.The whitelist entry has been correctly updated from partner-logo.svg to UN-logotype-gradient.svg, aligning with the changes to use dynamic path resolution in the auth-request modification.
api/src/unraid-api/unraid-file-modifier/modifications/auth-request.modification.ts (1)
9-9: Good approach using dynamic path resolution.The changes convert a hardcoded path to a dynamic, store-driven approach using the new utility function. This enhances maintainability and flexibility for managing partner logo paths.
Also applies to: 43-47
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (1)
1-470: Well-structured service with good separation of concerns.The CustomizationService is well-organized with clear separation between initialization, data retrieval, and applying different types of customizations. The error handling is comprehensive and the logging is thorough.
🧰 Tools
🪛 GitHub Check: Build API
[failure] 152-152:
Delete⏎
[failure] 152-152:
More than 1 blank line not allowed🪛 GitHub Check: Test API
[failure] 152-152:
Delete⏎
[failure] 152-152:
More than 1 blank line not allowedweb/composables/gql/graphql.ts (11)
25-26: Well-defined Port scalar type.This new scalar type for TCP ports correctly defines the input and output types as numbers, providing better type safety for port-related operations.
39-44: Good replacement for direct URL usage.The new
AccessUrlInputtype provides a more structured approach than directly using the URL scalar, with appropriate nullable fields for flexibility.
46-59: Clear activation code type structure.The new
ActivationCodetype has well-named fields that clearly represent the customization options available through activation codes.
279-292: Well-documented authentication enums.The new authentication-related enums have good documentation and clear, descriptive values.
424-425: Improved type safety for ContainerPort.Making these fields nullable and utilizing the new
Portscalar type improves the type safety of the API.
572-579: Added cache control capability.The new
skipCacheparameter gives clients better control over Docker data retrieval behavior.
448-452: Coherent customization structure.The new
CustomizationandPublicPartnerInfotypes, along with their associated query fields, create a logical structure for accessing activation code data and partner information.Also applies to: 1089-1097, 1107-1107, 1126-1126
1218-1218: Resource enum extension.Adding
ACTIVATION_CODEto the Resource enum aligns with the new activation code feature.
1718-1726: Well-defined TypeScript query types.The new TypeScript type definitions for PartnerInfo and ActivationCode queries match the schema changes and are properly structured.
1114-1115: New utility endpoints.The addition of
getDemoandhealthendpoints provides useful functionality for testing and monitoring.
823-823:Details
✅ Verification successful
Verify potential breaking changes in mutation restructuring.
The VM and parity check mutations have been restructured from direct fields on the Mutation type to grouped types. While this improves organization, it's a potential breaking change for existing clients.
Run the following script to check for client code that might be affected:
Also applies to: 835-835, 1043-1061, 1624-1675
🏁 Script executed:
#!/bin/bash # Look for any code that directly references VM or parity check mutations at the top level # Check for references to VM mutations echo "Checking for VM mutation references that might be affected:" rg -i "mutation.*\b(start|stop|pause|resume|reset|reboot|forceStop)(Vm|VirtualMachine)" --type ts # Check for references to parity check mutations echo -e "\nChecking for parity check mutation references that might be affected:" rg -i "mutation.*\b(start|pause|resume|cancel)(ParityCheck)" --type tsLength of output: 456
No internal usage of legacy root-level VM or parityCheck mutations detected. The grep scan for direct references (e.g. startVm, stopVm, cancelParityCheck) returned no matches, so this restructuring won’t break internal code.
• Affected file:
web/composables/gql/graphql.ts(lines 823, 835, 1043–1061, 1624–1675) now exposes mutations undervmandparityCheckgroups.
• No TypeScript or GraphQL documents in this repo reference the old top-level mutation fields.
• External clients should be notified to update their GraphQL calls to the new grouped fields and documentation/CHANGELOG updated accordingly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)
229-306: 🛠️ Refactor suggestionAddress the TODO comment about Redux store updates.
This TODO comment suggests that the state update might not be immediately reflected in the Redux store. Either implement the dispatch to update the Redux store or document why it's not needed.
await this.updateCfgFile(this.configFile, 'display', settingsToUpdate); this.logger.log('Display settings updated in config file.'); - // @todo: Consider dispatching an action to update Redux store if needed immediately + // Update Redux store to reflect the changed display settings + try { + const dynamixStore = getters.dynamix(); + if (dynamixStore?.display) { + store.dispatch({ + type: 'dynamix/updateDisplay', + payload: settingsToUpdate + }); + this.logger.log('Redux store updated with new display settings.'); + } + } catch (storeError) { + this.logger.warn('Failed to update Redux store:', storeError); + // Non-fatal error, continue + }
39-53: 🛠️ Refactor suggestionProvide a default value for missing configFile path.
The code correctly logs an error when the configFile path is missing, but still proceeds with initialization. It would be better to either throw an error or use a default path to prevent unexpected failures in downstream operations.
this.configFile = paths['dynamix-config']?.[1]; if (!this.configFile) { this.logger.error( "Could not resolve user dynamix config path (paths['dynamix-config'][1]) from store." ); - // Handle error appropriately - maybe throw, or use a default? - // For now, we'll let subsequent operations fail if configFile is needed. - return; // Stop initialization if critical path is missing + this.configFile = '/etc/local/emhttp/dynamix.cfg'; // Use a sensible default + this.logger.warn(`Using default config path: ${this.configFile}`); }
🧹 Nitpick comments (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)
119-145: Consider adding validation error details in the logs.When validation fails, it would be helpful to log the specific validation errors to aid in troubleshooting.
try { const fileContent = await fs.readFile(activationJsonPath, 'utf-8'); const activationDataRaw = JSON.parse(fileContent); const activationDataDto = plainToClass(ActivationCode, activationDataRaw); - await validateOrReject(activationDataDto); + try { + await validateOrReject(activationDataDto); + } catch (validationErrors) { + if (Array.isArray(validationErrors)) { + this.logger.error('Validation errors in activation data:', + validationErrors.map(err => Object.values(err.constraints || {})).flat()); + } + throw validationErrors; + } // Cache the validated data this.activationData = activationDataDto; this.logger.debug('Activation data fetched and cached.'); return this.activationData;
152-166: Consider adding path validation to prevent path traversal.The public methods that return web GUI paths should validate that the paths are within the expected bounds to prevent path traversal issues.
public async getCaseIconWebguiPath(): Promise<string | null> { const paths = getters.paths(); if (await fileExists(paths.caseModelSource)) { - return convertWebGuiPathToAssetPath(paths.caseModelTarget); + const assetPath = convertWebGuiPathToAssetPath(paths.caseModelTarget); + // Validate path is a subpath of the expected webgui directory + if (!paths.caseModelTarget.startsWith('/usr/local/emhttp/')) { + this.logger.warn(`Invalid webgui path detected: ${paths.caseModelTarget}`); + return null; + } + return assetPath; } return null; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
📒 Files selected for processing (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts(1 hunks)api/src/utils.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- api/src/utils.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (6)
web/composables/gql/graphql.ts (1)
ActivationCode(46-59)packages/unraid-api-plugin-connect/src/helpers/utils.ts (1)
fileExists(5-8)api/src/store/modules/paths.ts (4)
paths(112-116)partnerBannerSource(101-103)partnerBannerTarget(104-106)caseModelSource(95-97)api/src/store/index.ts (2)
getters(18-29)store(6-12)api/src/utils.ts (1)
convertWebGuiPathToAssetPath(280-284)api/src/core/utils/clients/emcmd.ts (1)
emcmd(12-41)
⏰ Context from checks skipped due to timeout of 90000ms (3)
- GitHub Check: Test API
- GitHub Check: Build API
- GitHub Check: Cloudflare Pages
🔇 Additional comments (11)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (11)
1-27: Well-organized imports and class setup.The imports are logically grouped and the class is well-structured with proper type definitions for member variables.
28-37: LGTM! First boot setup flag implementation is robust.The method correctly creates the activation directory if needed, checks for the existence of the flag file, and appropriately creates it if missing.
54-92: Robust initialization and activation directory handling.The initialization process correctly sets up paths, checks for directory existence, and handles the first boot setup flag. The error handling is comprehensive with appropriate logging.
94-112: Good path resolution with proper error handling.The method correctly finds the activation JSON file with appropriate error handling and logging.
146-150: Comprehensive error handling in activation data processing.The error handling for the activation data processing is well-implemented with appropriate logging and return values.
168-201: Robust customization application flow.The method appropriately checks for activation data, verifies directory existence, and applies customizations in a logical sequence with comprehensive error handling.
203-227: Well-implemented partner banner setup.The banner setup correctly handles file existence checks, copying, and error handling.
307-309: Good error handling for display settings.The error handling for display settings is appropriate with informative error logging.
311-357: Well-implemented case model configuration.The method effectively manages the case model configuration with appropriate checks, error handling, and logging.
359-412: Comprehensive server identity implementation.The server identity is properly applied with appropriate error handling, emcmd execution, and nginx/DNS updates.
414-468: Well-designed config file update utility.The updateCfgFile utility method is well-designed to handle both section-specific and root-level updates to INI files with proper error handling.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)
45-53: Provide a default value for missing configFile path.The code correctly logs an error when the configFile path is missing, but still proceeds with initialization. It would be better to either throw an error or use a default path to prevent unexpected failures in downstream operations.
this.configFile = paths['dynamix-config']?.[1]; if (!this.configFile) { this.logger.error( "Could not resolve user dynamix config path (paths['dynamix-config'][1]) from store." ); - // Handle error appropriately - maybe throw, or use a default? - // For now, we'll let subsequent operations fail if configFile is needed. - return; // Stop initialization if critical path is missing + this.configFile = '/etc/local/emhttp/dynamix.cfg'; // Use a sensible default + this.logger.warn(`Using default config path: ${this.configFile}`); }
28-37: 🛠️ Refactor suggestionEnhance createOrGetFirstBootSetupFlag method to handle uninitialized paths.
This method relies on
this.activationDirbeing initialized, but could fail if called beforeonModuleInit. Consider accepting an optional parameter or checking for initialization.-async createOrGetFirstBootSetupFlag(): Promise<boolean> { +async createOrGetFirstBootSetupFlag(activationDirOverride?: string): Promise<boolean> { + const dirToUse = activationDirOverride || this.activationDir; + if (!dirToUse) { + throw new Error('Activation directory path not initialized'); + } - await fs.mkdir(this.activationDir, { recursive: true }); - if (await fileExists(this.hasRunFirstBootSetup)) { + await fs.mkdir(dirToUse, { recursive: true }); + const flagPath = activationDirOverride ? + path.join(dirToUse, '.done') : + this.hasRunFirstBootSetup; + if (await fileExists(flagPath)) { this.logger.log('First boot setup flag file already exists.'); return true; // Indicate setup was already done based on flag presence } - await fs.writeFile(this.hasRunFirstBootSetup, 'true'); + await fs.writeFile(flagPath, 'true'); this.logger.log('First boot setup flag file created.'); return false; // Indicate setup was just marked as done }
🧹 Nitpick comments (4)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (4)
177-186: Simplify redundant directory check.This directory check is redundant since it's already performed in
onModuleInit. Consider removing it or adding a comment explaining why it's necessary to check again.- // Check if activation dir exists (redundant if onModuleInit succeeded, but safe) - try { - await fs.access(this.activationDir); - } catch (dirError: any) { - if (dirError.code === 'ENOENT') { - this.logger.warn('Activation directory disappeared after init? Skipping.'); - return; - } - throw dirError; // Rethrow other errors - }
243-267: Move type definition outside method for reusability.The
DisplayMappingtype is defined within the method scope, limiting its reusability. Consider moving it to the class level or exporting it from a shared types file.+// Define types at the class or module level +type DisplayMapping = { + key: string; + transform?: (v: any) => string; + skipIfEmpty?: boolean; +}; @Injectable() export class CustomizationService implements OnModuleInit { // ... existing properties private async applyDisplaySettings() { // ... existing code // Map activation data properties to their corresponding config keys - type DisplayMapping = { - key: string; - transform?: (v: any) => string; - skipIfEmpty?: boolean; - }; const displayMappings: Record<string, DisplayMapping> = { // ... existing mappings };
269-277: Replace forEach with a safer reduce operation.Using
forEachwith side effects can be error-prone. Consider usingreducefor a more functional approach that is easier to test and reason about.- Object.entries(displayMappings).forEach(([prop, mapping]) => { - const value = this.activationData?.[prop]; - if (value !== undefined && value !== null) { - const transformedValue = mapping.transform ? mapping.transform(value) : value; - if (!mapping.skipIfEmpty || transformedValue) { - settingsToUpdate[mapping.key] = transformedValue; - } - } - }); + const settingsToUpdate = Object.entries(displayMappings).reduce((acc, [prop, mapping]) => { + const value = this.activationData?.[prop]; + if (value !== undefined && value !== null) { + const transformedValue = mapping.transform ? mapping.transform(value) : value; + if (!mapping.skipIfEmpty || transformedValue) { + acc[mapping.key] = transformedValue; + } + } + return acc; + }, {} as Record<string, string>);
419-433: Improve error handling for config file reading.The error handling when reading the config file could be more robust. Consider explicitly checking for file existence before attempting to read it.
let configData: any = {}; // Use 'any' for flexibility with ini structure try { + // Check if file exists first + if (await fileExists(filePath)) { const content = await fs.readFile(filePath, 'utf-8'); // Parse the INI file content. Note: ini library parses values as strings by default. // It might interpret numbers/booleans if not quoted, but our values are always quoted. configData = ini.parse(content); + } else { + this.logger.log(`Config file ${filePath} not found, will create it.`); + } } catch (error: any) { - if (error.code === 'ENOENT') { - this.logger.log(`Config file ${filePath} not found, will create it.`); - // Initialize configData as an empty object if file doesn't exist - } else { - this.logger.error(`Error reading config file ${filePath}:`, error); - throw error; // Re-throw other errors - } + this.logger.error(`Error reading config file ${filePath}:`, error); + throw error; // Re-throw errors }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
📒 Files selected for processing (1)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (4)
- GitHub Check: Test API
- GitHub Check: Build Web App
- GitHub Check: Build API
- GitHub Check: Cloudflare Pages
api/src/unraid-api/graph/resolvers/customization/customization.service.ts
Show resolved
Hide resolved
api/src/unraid-api/graph/resolvers/customization/customization.service.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (3)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (3)
17-18:⚠️ Potential issueDuplicate
vi.mock– second factory silently ignored
vi.mock('@app/core/utils/clients/emcmd.js')is declared twice.
Because Vitest hoists mocks, the first (auto-mock) wins and the factory at lines 47-68 is never applied, leavingemcmdan empty stub.-vi.mock('@app/core/utils/clients/emcmd.js'); - -… -vi.mock('@app/core/utils/clients/emcmd.js', async () => { +vi.mock('@app/core/utils/clients/emcmd.js', async () => {Remove the first call (or merge them) so your custom mock is actually used.
Also applies to: 47-48
121-128: 🛠️ Refactor suggestionManual re-assignment hides initialization bugs
Reassigning internal paths after service construction bypasses the very behaviour the test is meant to verify (initialisation from the store). It also forces every test to repeat this boilerplate.
Better: stub
store.getState()(or the relevant getters) to returnmockPathsand letonModuleInitpopulate these fields naturally.This was pointed out in a previous review and is still unresolved.
786-789:⚠️ Potential issueSpies registered after the call never fire
setupPartnerBannerSpyandapplyDisplaySettingsSpyare created afterapplyActivationCustomizations()has already executed, so they will always report “not called” even if the methods actually ran.Move the spy creation before invoking the method:
-await (service as any).applyActivationCustomizations(); -const setupPartnerBannerSpy = vi.spyOn(service as any, 'setupPartnerBanner'); -const applyDisplaySettingsSpy = vi.spyOn(service as any, 'applyDisplaySettings'); +const setupPartnerBannerSpy = vi.spyOn(service as any, 'setupPartnerBanner'); +const applyDisplaySettingsSpy = vi.spyOn(service as any, 'applyDisplaySettings'); +await (service as any).applyActivationCustomizations();This makes the assertion meaningful.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
📒 Files selected for processing (1)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts(1 hunks)
🧰 Additional context used
🪛 GitHub Check: Test API
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts
[failure] 701-701:
Replace 'Error·applying·server·identity:·%o',·emcmdError with ⏎················'Error·applying·server·identity:·%o',⏎················emcmdError⏎············
🪛 GitHub Check: Build API
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts
[failure] 701-701:
Replace 'Error·applying·server·identity:·%o',·emcmdError with ⏎················'Error·applying·server·identity:·%o',⏎················emcmdError⏎············
🪛 GitHub Actions: CI - Main (API)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts
[error] 701-701: Prettier formatting error: Replace 'Error applying server identity: %o', emcmdError with a properly formatted multiline string as per prettier/prettier rule.
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: Cloudflare Pages
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
♻️ Duplicate comments (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (2)
121-128: Manual path re-assignment masks init bugs
This test suite still overwritesactivationDir,configFile, etc., after creating the service instance. As noted in the previous review, doing so defeats the purpose of verifying thatonModuleInitcorrectly reads the store.Please fix the store mock instead of mutating internal state.
15-19:⚠️ Potential issueDuplicate mock declarations – only the first one takes effect
vi.mock('@app/core/utils/clients/emcmd.js')is declared twice (lines 17 and 47).
Vitest hoists mocks; the factory on lines 47-68 is silently ignored, leavingemcmdas an empty stub and invalidating several assertions.Delete the first call:
-vi.mock('@app/core/utils/clients/emcmd.js');Retain the factory version to keep the typed mock response.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
📒 Files selected for processing (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.service.ts(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (2)
- GitHub Check: Build and Deploy Plugin / Build and Deploy Plugin
- GitHub Check: Cloudflare Pages
api/src/unraid-api/graph/resolvers/customization/customization.service.ts
Outdated
Show resolved
Hide resolved
api/src/unraid-api/graph/resolvers/customization/customization.service.ts
Outdated
Show resolved
Hide resolved
api/src/unraid-api/graph/resolvers/customization/customization.service.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (2)
17-18:⚠️ Potential issueDuplicate
vi.mockforemcmd– the custom factory is silently ignoredVitest hoists mocks; the first
vi.mock('@app/core/utils/clients/emcmd.js')(line 17) wins and the factory in lines 50-71 is never used, leavingemcmdas an empty stub and breaking assertions that rely on the mocked response.-vi.mock('@app/core/utils/clients/emcmd.js');Delete the first call (or merge the two) so the factory takes effect.
Also applies to: 50-71
124-131: 🛠️ Refactor suggestionManual property re-assignment hides real-world failures
The service is supposed to derive
activationDir,configFile, etc. from the mocked store duringonModuleInit, yet the tests overwrite those fields right after instantiation. This masks bugs in the initialisation path.Instead, fix the store mock so the service picks up the correct values automatically and drop these lines altogether.
-// Re-assign paths manually in beforeEach AFTER mocks are cleared and service instantiated -(service as any).activationDir = activationDir; -(service as any).hasRunFirstBootSetup = doneFlag; -(service as any).configFile = userDynamixCfg; -(service as any).caseModelCfg = caseModelCfg; -(service as any).identCfg = identCfg;
🧹 Nitpick comments (4)
package.json (3)
29-33: Re-evaluateonlyBuiltDependenciesentriesDev‐only tools like
simple-git-hookstypically shouldn’t be listed here—it may bloat your production install. Consider restricting this array to true runtime dependencies (e.g.,core-js,es5-ext,protobufjs) and removing dev-only packages.
48-50: Configuresimple-git-hookspre-commit hookThe
pre-commithook is correctly defined. You might also consider adding hooks likecommit-msgfor message linting orpre-pushfor additional checks.
51-55: Extendlint-stagedfile patternsYour current globs target JS/TS only. To cover the full monorepo, consider including
.vue,.json, or other relevant formats so all staged files get linted.api/src/core/utils/clients/emcmd.ts (1)
12-15: Consider caching the dynamic import to avoid per-call overhead
import()is cached by Node, but you still pay theawaitcost on every invocation.
Ifemcmdis called frequently, the extra micro-await adds latency. A tiny wrapper keeps the laziness you want while paying the price only once:-export const emcmd = async (commands: LooseObject) => { - const { getters } = await import('@app/store/index.js'); +let cachedGetters: typeof import('@app/store/index.js').getters | null = null; +export const emcmd = async (commands: LooseObject) => { + if (!cachedGetters) { + ({ getters: cachedGetters } = await import('@app/store/index.js')); + } + const getters = cachedGetters!;Nice and simple, no global side-effects.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (5)
.husky/_/pre-commit(1 hunks)api/src/core/utils/clients/emcmd.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.service.ts(1 hunks)package.json(3 hunks)
✅ Files skipped from review due to trivial changes (1)
- .husky/_/pre-commit
🚧 Files skipped from review as they are similar to previous changes (1)
- api/src/unraid-api/graph/resolvers/customization/customization.service.ts
⏰ Context from checks skipped due to timeout of 90000ms (2)
- GitHub Check: Build and Deploy Plugin / Build and Deploy Plugin
- GitHub Check: Cloudflare Pages
🔇 Additional comments (2)
package.json (2)
15-16: Verify new scripts and postinstall behaviorConfirm that
sync-webgui-repohandles errors and is idempotent, and ensure that runningsimple-git-hooksin thepostinstallhook doesn’t conflict with other install scripts or sequences.
44-47: Addlint-stagedandsimple-git-hooksas devDependenciesGood move to centralize pre-commit tooling. These devDependencies align well with the new Git-hook setup.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (6)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (6)
16-19: Duplicate mocks foremcmd- only the first one is honoredThere are two separate mock declarations for the same module. Vitest hoists mocks, so the first mock (auto-mock) is used and the detailed factory mock is ignored.
-vi.mock('@app/core/utils/clients/emcmd.js');Keep the factory mock for proper response handling.
38-48: Mockingstoredoes not overridestore.getState()The current mock only adds a
gettersfield but doesn't provide agetState()implementation, which is what the service actually uses.vi.mock('@app/store/index.js', async () => { const actual = await vi.importActual('@app/store/index.js'); return { ...actual, + store: { + getState: () => ({ paths: mockPaths }), + }, getters: { paths: vi.fn(() => mockPaths), dynamix: vi.fn(() => mockDynamixState), emhttp: vi.fn(() => mockEmhttpState), }, }; });
125-131: Manual path re-assignment makes tests less reliableService paths are manually reassigned after instantiation, which means tests aren't accurately verifying initialization logic. This approach hides potential initialization issues.
- // Re-assign paths manually in beforeEach AFTER mocks are cleared and service instantiated - // This simulates the dynamic import within onModuleInit - (service as any).activationDir = activationDir; - (service as any).hasRunFirstBootSetup = doneFlag; - (service as any).configFile = userDynamixCfg; - (service as any).caseModelCfg = caseModelCfg; - (service as any).identCfg = identCfg;Fix the store mock as suggested above instead.
148-164: Complex approach for testing missing config pathThe test for handling a missing dynamix user config path is overly complex. You're temporarily modifying the mockPaths object and then restoring it, which is fragile.
Consider using the
mockImplementationOnceapproach instead to temporarily change the behavior for a single test:it('should log error if dynamix user config path is missing', async () => { - // Temporarily modify mockPaths to simulate missing user config path - const originalPaths = { ...mockPaths }; - mockPaths['dynamix-config'] = [mockPaths['dynamix-config'][0]]; // Only keep default config + // Save original mock to restore later + const originalGetState = vi.mocked(store.getState); + + // Mock store to return paths without dynamix-config[1] + vi.mocked(store.getState).mockReturnValueOnce({ + paths: { + ...mockPaths, + 'dynamix-config': [mockPaths['dynamix-config'][0]], + } + }); await service.onModuleInit(); expect(loggerErrorSpy).toHaveBeenCalledWith( "Could not resolve user dynamix config path (paths['dynamix-config'][1]) from store." ); // Expect subsequent operations that rely on configFile to fail or not run expect(fs.writeFile).not.toHaveBeenCalledWith(doneFlag, 'true'); // Setup should bail early - // Restore original paths - mockPaths['dynamix-config'] = originalPaths['dynamix-config']; + // Restore original mock + vi.mocked(store.getState).mockImplementation(originalGetState); });
820-826: Ineffective test setup - spies registered after method callThe spies on
setupPartnerBannerandapplyDisplaySettingsare registered afterapplyActivationCustomizationshas already been called, making the assertions ineffective.+ // Set up spies BEFORE calling the method + const setupPartnerBannerSpy = vi.spyOn(service as any, 'setupPartnerBanner'); + const applyDisplaySettingsSpy = vi.spyOn(service as any, 'applyDisplaySettings'); await (service as any).applyActivationCustomizations(); expect(loggerWarnSpy).toHaveBeenCalledWith( 'Activation directory disappeared after init? Skipping.' ); // Ensure no customization methods were called implicitly by checking their side effects - const setupPartnerBannerSpy = vi.spyOn(service as any, 'setupPartnerBanner'); - const applyDisplaySettingsSpy = vi.spyOn(service as any, 'applyDisplaySettings'); expect(setupPartnerBannerSpy).not.toHaveBeenCalled(); expect(applyDisplaySettingsSpy).not.toHaveBeenCalled();
701-724: Prettier formatting will break CI for error messageThe long single-line error expectation will fail Prettier formatting checks in CI.
-expect(loggerErrorSpy).toHaveBeenCalledWith('Error applying server identity: %o', emcmdError); +expect(loggerErrorSpy).toHaveBeenCalledWith( + 'Error applying server identity: %o', + emcmdError, +);Similar formatting issues may exist elsewhere in the file.
🧹 Nitpick comments (4)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (4)
4-4: Consider adding URL validation forpartnerUrlYou're importing various validators from
class-validatorbut only usingIsStringforpartnerUrlfields. Consider addingIsUrlvalidation to ensure valid URLs are provided.- import { IsBoolean, IsIn, IsOptional, IsString, IsUrl } from 'class-validator'; + import { IsBoolean, IsIn, IsOptional, IsString, IsUrl } from 'class-validator';Then apply it to the partnerUrl fields:
@Field(() => String, { nullable: true }) @IsOptional() @IsString() + @IsUrl() @Transform(({ value }) => sanitizeString(value)) partnerUrl?: string;
6-7: Hex color validation function could be more robustThe current regex only matches exact 3 or 6 character hex colors. Consider adding support for 4 and 8 character hex codes (which include alpha channel) or using a library like
colorfor more robust validation.
18-31: Enhance hex color validation with more informative error handlingThe current implementation silently returns an empty string for invalid colors. Consider adding a warning log or throwing a specific error that can be caught and handled appropriately.
const sanitizeAndValidateHexColor = (value: any): string => { let sanitized = sanitizeString(value); if (typeof sanitized === 'string') { // Check if it's a 3 or 6 character hex string without '#' if (/^([0-9A-F]{3}){1,2}$/i.test(sanitized)) { sanitized = `#${sanitized}`; // Prepend '#' } // Now validate if it's a standard hex color format if (isHexColor(sanitized)) { return sanitized; } + // Log or output more information about the invalid input + console.warn(`Invalid hex color format: ${value}`); } return ''; // Return empty string if not a valid hex color after potential modification };
115-122: Consider adding type safety for boolean transformationThe current transformation converts string 'yes' to boolean true, but doesn't handle other truthy/falsy string values like 'true', '1', etc. Consider using a more comprehensive approach for boolean conversion.
@Field(() => Boolean, { nullable: true }) @IsOptional() @Transform(({ value }) => { if (typeof value === 'boolean') return value; const sanitized = sanitizeString(value); - return sanitized === 'yes'; + return ['yes', 'true', '1', 'on'].includes(sanitized.toLowerCase()); }) showBannerGradient?: boolean = true;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
📒 Files selected for processing (2)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (1)
web/composables/gql/graphql.ts (3)
PublicPartnerInfo(1089-1097)ActivationCode(46-59)Customization(448-452)
⏰ Context from checks skipped due to timeout of 90000ms (4)
- GitHub Check: Test API
- GitHub Check: Build API
- GitHub Check: Build Web App
- GitHub Check: Cloudflare Pages
🔇 Additional comments (7)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (3)
82-83: Good use of length limitation for serverNameThe 15-character limit for
serverNameis good practice, as it prevents potential buffer overflows or UI issues in downstream systems.
124-128: Good use of IsIn validator for theme propertyUsing
@IsInvalidator with specific allowed theme values ensures that only valid themes can be applied, preventing errors in the UI.
131-138: Customization class provides a clean aggregation structureThe
Customizationclass effectively aggregates related data models, making the GraphQL schema more organized and easier to query.api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (4)
395-412: Good test for handling invalid hex colorsThe test correctly verifies that invalid hex colors are transformed to empty strings, which allows validation to pass while preventing invalid data from propagating.
414-430: Excellent test for hex color normalizationThis test properly verifies that hex colors provided without the '#' prefix are correctly normalized, ensuring consistent data format.
474-482: Comprehensive test for partner banner setupThe test thoroughly validates the banner setup process, including file copying and appropriate logging.
959-1112: Well-structured unit tests for the updateCfgFile utilityThe test suite for updateCfgFile is thorough and covers various scenarios:
- Creating new files with and without sections
- Merging updates with existing content
- Adding new sections to existing files
- Error handling for file operations
This ensures the configuration management is robust and reliable.
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (12)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (7)
46-54: Provide a default value for missing configFile path.The code correctly logs an error when the configFile path is missing, but still proceeds with initialization. It would be better to either throw an error or use a default path to prevent unexpected failures in downstream operations.
this.configFile = paths['dynamix-config']?.[1]; if (!this.configFile) { this.logger.error( "Could not resolve user dynamix config path (paths['dynamix-config'][1]) from store." ); - // Handle error appropriately - maybe throw, or use a default? - // For now, we'll let subsequent operations fail if configFile is needed. - return; // Stop initialization if critical path is missing + this.configFile = '/etc/local/emhttp/dynamix.cfg'; // Use a sensible default + this.logger.warn(`Using default config path: ${this.configFile}`); }
153-159: Add path validation to prevent path traversal issues.The method
getCaseIconWebguiPathshould validate paths to ensure they're within expected bounds before converting them to web asset paths.public async getCaseIconWebguiPath(): Promise<string | null> { const paths = getters.paths(); if (await fileExists(paths.caseModelSource)) { + // Validate path is within expected bounds + if (!paths.caseModelTarget.startsWith('/usr/local/emhttp/')) { + this.logger.warn(`Invalid target path: ${paths.caseModelTarget}`); + return null; + } return convertWebGuiPathToAssetPath(paths.caseModelTarget); } return null; }
231-240: Remove redundant dynamic import.The code re-imports
gettersdespite it already being statically imported at the top of the file. This adds unnecessary I/O and module initialization overhead.-const { getters } = await import('@app/store/index.js'); if (!this.activationData) { this.logger.warn('No activation data available for display settings.'); return; } this.logger.log('Applying display settings...'); -const currentDisplaySettings = getters.dynamix()?.display || {}; +const currentDisplaySettings = getters.dynamix()?.display || {}; this.logger.debug('Current display settings from store:', currentDisplaySettings);
250-267: Add type safety for activation data properties.The code assumes the activation data properties are of the expected types for the transformations. Consider adding explicit type checks for better safety.
const displayMappings: Record<string, DisplayMapping> = { - header: { key: 'header', transform: (v: string) => v.replace('#', ''), skipIfEmpty: true }, + header: { + key: 'header', + transform: (v: string) => typeof v === 'string' ? v.replace('#', '') : '', + skipIfEmpty: true + }, - headermetacolor: { - key: 'headermetacolor', - transform: (v: string) => v.replace('#', ''), - skipIfEmpty: true, - }, + headermetacolor: { + key: 'headermetacolor', + transform: (v: string) => typeof v === 'string' ? v.replace('#', '') : '', + skipIfEmpty: true, + }, // Apply similar changes to other properties
283-290: Banner existence check targets wrong file.The code verifies
partnerBannerSource, i.e. the file before it is copied. If the copy fails, we would still forcebanner=image, resulting in a broken UI.-const partnerBannerSource = paths.partnerBannerSource; -... -if (await fileExists(partnerBannerSource)) { +const partnerBannerTarget = paths.partnerBannerTarget; +... +if (await fileExists(partnerBannerTarget)) {
420-467: Missing directory-existence guard may break first boot.
updateCfgFileblindly callsfs.writeFile(filePath, …); if the parent directory is absent the write will throwENOENT.+await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, newContent + '\n');
161-167: 🛠️ Refactor suggestionAdd path validation to prevent path traversal issues in getPartnerLogoWebguiPath.
Similar to the getCaseIconWebguiPath method, this function should validate paths to ensure they're within expected bounds.
public async getPartnerLogoWebguiPath(): Promise<string | null> { const paths = getters.paths(); if (await fileExists(paths.partnerLogoSource)) { + // Validate path is within expected bounds + if (!paths.partnerLogoTarget.startsWith('/usr/local/emhttp/')) { + this.logger.warn(`Invalid target path: ${paths.partnerLogoTarget}`); + return null; + } return convertWebGuiPathToAssetPath(paths.partnerLogoTarget); } return null; }api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (5)
16-19: Duplicate mocks foremcmd– only the first one is honoured.
vi.mock('@app/core/utils/clients/emcmd.js')is declared twice. Vitest hoists mocks, so the first statement (auto-mock) wins and the factory at lines 50-71 is silently ignored, leavingemcmdas an empty stub.-vi.mock('@app/core/utils/clients/emcmd.js'); - ...
38-48: Mockingstoredoes not overridestore.getState().
CustomizationServicecallsstore.getState()via getters, but this mock only adds agettersfield. Provide a minimal stub forgetState:vi.mock('@app/store/index.js', async () => { const actual = await vi.importActual('@app/store/index.js'); return { ...actual, + store: { + getState: () => ({ paths: mockPaths }), + }, getters: { paths: vi.fn(() => mockPaths), dynamix: vi.fn(() => mockDynamixState), emhttp: vi.fn(() => mockEmhttpState), }, }; });
131-137: Manual path re-assignment makes tests less reliable.The service paths are manually reassigned after instantiation, which means the tests aren't accurately verifying initialization from the store. This approach is error-prone and hides initialization issues.
Remove these manual assignments and fix the store mock instead, as suggested in the previous comment.
826-832: Ineffective test setup - spies registered after method call.The spies on
setupPartnerBannerandapplyDisplaySettingsare registered afterapplyActivationCustomizationshas already been called, making the assertions ineffective.+ // Set up spies BEFORE calling the method + const setupPartnerBannerSpy = vi.spyOn(service as any, 'setupPartnerBanner'); + const applyDisplaySettingsSpy = vi.spyOn(service as any, 'applyDisplaySettings'); await (service as any).applyActivationCustomizations(); expect(loggerWarnSpy).toHaveBeenCalledWith( 'Activation directory disappeared after init? Skipping.' ); // Ensure no customization methods were called implicitly by checking their side effects - const setupPartnerBannerSpy = vi.spyOn(service as any, 'setupPartnerBanner'); - const applyDisplaySettingsSpy = vi.spyOn(service as any, 'applyDisplaySettings'); expect(setupPartnerBannerSpy).not.toHaveBeenCalled(); expect(applyDisplaySettingsSpy).not.toHaveBeenCalled();
701-731: Fix the multi-line expect statement formatting.CI fails because this long
.toHaveBeenCalledWithline exceeds the formatter rules.-expect(loggerErrorSpy).toHaveBeenCalledWith('Error applying server identity: %o', emcmdError); +expect(loggerErrorSpy).toHaveBeenCalledWith( + 'Error applying server identity: %o', + emcmdError +);
🧹 Nitpick comments (3)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)
359-411: Consider separating server identity application from UI reload.The
applyServerIdentitymethod combines two distinct operations: updating server identity configuration and reloading the web UI. This violates the Single Responsibility Principle. Consider separating these operations for better maintainability.private async applyServerIdentity() { const { getters, store } = await import('@app/store/index.js'); if (!this.activationData) { this.logger.warn('No activation data available for server identity setup.'); return; } this.logger.log('Applying server identity...'); // ... existing code ... try { // Update ident.cfg first await this.updateCfgFile(this.identCfg, null, paramsToUpdate); this.logger.log(`Server identity updated in ${this.identCfg}`); // Trigger emhttp update via emcmd const updateParams = { ...paramsToUpdate, changeNames: 'Apply' }; this.logger.log(`Calling emcmd with params: %o`, updateParams); await sleep(10000); await emcmd(updateParams); this.logger.log('emcmd executed successfully.'); + + await this.reloadWebUI(); + } catch (error) { + this.logger.error('Error applying server identity: %o', error); + } +} + +// Separate method for UI reload +private async reloadWebUI() { + try { + const { store } = await import('@app/store/index.js'); await store.dispatch(reloadNginxAndUpdateDNS()); this.logger.log('Nginx reloaded and DNS updated successfully.'); } catch (error) { - this.logger.error('Error applying server identity: %o', error); + this.logger.error('Error reloading web UI: %o', error); } }
402-404: Document the reason for the 10-second sleep.The code includes a significant 10-second sleep before executing the emcmd command. Without explanation, this delay seems excessive and could confuse future developers.
+ // A 10-second delay is needed before executing emcmd to ensure + // that system has enough time to process previous configuration changes await sleep(10000); await emcmd(updateParams);api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1)
139-147: Enhance fileExists mock for more robust tests.The current
fileExistsmock implementation only covers a limited set of specific paths. Consider a more comprehensive approach that handles all possible paths that might be checked during test execution.// Mock fileExists needed by customization methods vi.mocked(fileExists).mockImplementation(async (p) => { // Assume assets exist by default for these tests unless overridden - return ( - p === partnerBannerSource || - p === bannerAssetPath || - p === caseModelAssetPath || - p === bannerDestPath - ); + // Handle specific test-critical paths explicitly + if ([partnerBannerSource, bannerAssetPath, caseModelAssetPath, bannerDestPath].includes(p)) { + return true; + } + + // Handle common path patterns + if (p.includes('/assets/') && !p.includes('.done')) { + return true; // Most asset files exist by default + } + + if (p === doneFlag) { + return false; // .done flag doesn't exist by default + } + + // Log unexpected paths to help identify missed mocks + console.log(`Unmocked fileExists call for path: ${p}`); + return false; // Default to false for safety });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
📒 Files selected for processing (4)
api/src/core/utils/clients/emcmd.ts(2 hunks)api/src/environment.ts(0 hunks)api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.service.ts(1 hunks)
💤 Files with no reviewable changes (1)
- api/src/environment.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- api/src/core/utils/clients/emcmd.ts
⏰ Context from checks skipped due to timeout of 90000ms (4)
- GitHub Check: Build Web App
- GitHub Check: Build API
- GitHub Check: Test API
- GitHub Check: Cloudflare Pages
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (5)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (5)
46-54: Provide a default value for missing configFile path.The code correctly logs an error when the configFile path is missing, but still proceeds with initialization. It would be better to either throw an error or use a default path to prevent unexpected failures in downstream operations.
this.configFile = paths['dynamix-config']?.[1]; if (!this.configFile) { this.logger.error( "Could not resolve user dynamix config path (paths['dynamix-config'][1]) from store." ); - // Handle error appropriately - maybe throw, or use a default? - // For now, we'll let subsequent operations fail if configFile is needed. - return; // Stop initialization if critical path is missing + this.configFile = '/etc/local/emhttp/dynamix.cfg'; // Use a sensible default + this.logger.warn(`Using default config path: ${this.configFile}`); }
153-159: Add path validation to prevent path traversal issues.Methods like
getCaseIconWebguiPathshould validate paths to prevent potential path traversal vulnerabilities when converting paths.public async getCaseIconWebguiPath(): Promise<string | null> { const paths = getters.paths(); if (await fileExists(paths.caseModelSource)) { + // Validate path is within expected bounds + if (!paths.caseModelTarget.startsWith('/usr/local/emhttp/')) { + this.logger.warn(`Invalid target path: ${paths.caseModelTarget}`); + return null; + } return convertWebGuiPathToAssetPath(paths.caseModelTarget); } return null; }
249-266: Add type safety for activation data properties.The code assumes the activation data properties are of the expected types for the transformations. Consider adding explicit type checks for better safety.
const displayMappings: Record<string, DisplayMapping> = { - header: { key: 'header', transform: (v: string) => v.replace('#', ''), skipIfEmpty: true }, + header: { + key: 'header', + transform: (v: string) => typeof v === 'string' ? v.replace('#', '') : '', + skipIfEmpty: true + }, - headermetacolor: { - key: 'headermetacolor', - transform: (v: string) => v.replace('#', ''), - skipIfEmpty: true, - }, + headermetacolor: { + key: 'headermetacolor', + transform: (v: string) => typeof v === 'string' ? v.replace('#', '') : '', + skipIfEmpty: true, + }, // Apply similar changes to other properties
282-289: Banner existence check targets wrong file.The code verifies
partnerBannerSource, i.e. the file before it is copied. If the copy fails (disk full, permissions, etc.) we would still forcebanner=image, resulting in a broken UI.-const partnerBannerSource = paths.partnerBannerSource; -... -if (await fileExists(partnerBannerSource)) { +const partnerBannerTarget = paths.partnerBannerTarget; +... +if (await fileExists(partnerBannerTarget)) {
412-459: Missing directory-existence guard may break first boot.
updateCfgFileblindly callsfs.writeFile(filePath, …); if the parent directory is absent (/mock/useron fresh installs, read-only mounts, etc.) the write will throwENOENT.+await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, newContent + '\n');
🧹 Nitpick comments (2)
api/src/core/utils/clients/emcmd.ts (1)
2-2: Unnecessary import of ArrayDisk.The
ArrayDiskimport on line 11 appears to be unused in this file and can be safely removed.-import { ArrayDisk } from '@app/unraid-api/graph/resolvers/array/array.model.js';api/src/unraid-api/graph/resolvers/customization/customization.service.ts (1)
368-375: Consider optimizing state access pattern.You're retrieving values from the Redux store in multiple places. Consider extracting this into a reusable helper function or memoizing the values to avoid repeated access.
+private getIdentityValues() { + const currentEmhttpState = getters.emhttp(); + return { + currentName: currentEmhttpState?.var?.name || '', + currentSysModel: currentEmhttpState?.var?.sysModel || '', + currentComment: currentEmhttpState?.var?.comment || '' + }; +} private async applyServerIdentity() { // ... - const currentEmhttpState = getters.emhttp(); - const currentName = currentEmhttpState?.var?.name || ''; - const currentSysModel = currentEmhttpState?.var?.sysModel || ''; - const currentComment = currentEmhttpState?.var?.comment || ''; + const { currentName, currentSysModel, currentComment } = this.getIdentityValues(); // ... }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
📒 Files selected for processing (3)
api/src/core/utils/clients/emcmd.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.service.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts
⏰ Context from checks skipped due to timeout of 90000ms (4)
- GitHub Check: Test API
- GitHub Check: Build API
- GitHub Check: Build Web App
- GitHub Check: Cloudflare Pages
🔇 Additional comments (5)
api/src/core/utils/clients/emcmd.ts (4)
20-20: Dynamic import pattern is good for preventing circular dependencies.The dynamic import of getters helps prevent potential circular dependencies.
23-25: Good error handling for missing socket path.Adding an explicit check for socketPath improves error detection and provides a clear error message.
27-54: Robust CSRF token retry implementation.The retry logic with exponential backoff is well-implemented. It provides resilience when tokens aren't immediately available during system startup.
56-57: HTTP method changed from GET to POST.Changing from
got.gettogot.postmay have implications for server-side handlers.Verify that server-side handlers for
/update.htmproperly handle POST requests with the same parameter structure that was previously sent with GET.api/src/unraid-api/graph/resolvers/customization/customization.service.ts (1)
399-399: Good use of waitForToken parameter.The
emcmdfunction is called with{ waitForToken: true }, ensuring that the CSRF token is available before attempting the command execution. This aligns well with the changes made to the emcmd utility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (1)
278-285: Banner existence check still targets source instead of target
applyDisplaySettingsverifiespaths.partnerBannerSourceafter the copy, which does not guarantee that the banner was actually written to the Web-GUI images directory. The resilient check is to look atpaths.partnerBannerTarget(or the computedbannerDestPath) so the UI only switches tobanner=imagewhen the destination file is present.-const partnerBannerSource = paths.partnerBannerSource; -... -if (await fileExists(partnerBannerSource)) { - settingsToUpdate['banner'] = 'image'; - this.logger.debug(`Webgui banner exists at ${partnerBannerSource}, setting banner=image.`); +const partnerBannerTarget = paths.partnerBannerTarget; +... +if (await fileExists(partnerBannerTarget)) { + settingsToUpdate['banner'] = 'image'; + this.logger.debug(`Webgui banner exists at ${partnerBannerTarget}, setting banner=image.`); }api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1)
130-136: Manual path re-assignment hides initialisation bugsRe-populating
activationDir,configFile, … after instantiating the service means the tests still pass even ifonModuleInitfails to read the store – exactly the false-positive noted in previous reviews. Refactor the mock store soonModuleInitpicks up accurate paths and drop this manual reassignment.
🧹 Nitpick comments (1)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1)
536-550: Assertion hard-codes every field – considerobjectContainingThese lines assert the full object produced by
applyDisplaySettings. Any future field (e.g. a new display option) will break the test unnecessarily. Prefer:expect(updateSpy).toHaveBeenCalledWith( userDynamixCfg, 'display', expect.objectContaining({ header: '112233', theme: 'black', banner: 'image', }) );to keep the test focused on behaviour, not implementation details.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
📒 Files selected for processing (3)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.service.ts(1 hunks)api/src/unraid-api/unraid-file-modifier/modifications/case-model-copier.modification.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- api/src/unraid-api/unraid-file-modifier/modifications/case-model-copier.modification.ts
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: Cloudflare Pages
api/src/unraid-api/graph/resolvers/customization/customization.service.ts
Outdated
Show resolved
Hide resolved
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts
Outdated
Show resolved
Hide resolved
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts
Outdated
Show resolved
Hide resolved
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts
Show resolved
Hide resolved
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts
Outdated
Show resolved
Hide resolved
api/src/unraid-api/unraid-file-modifier/modifications/case-model-copier.modification.ts
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)
266-283: Add type safety for activation data propertiesThe code assumes the activation data properties are of the expected types for the transformations. Consider adding explicit type checks for better safety.
const displayMappings: Record<string, DisplayMapping> = { - header: { key: 'header', transform: (v: string) => v.replace('#', ''), skipIfEmpty: true }, + header: { + key: 'header', + transform: (v: string) => typeof v === 'string' ? v.replace('#', '') : '', + skipIfEmpty: true + }, - headermetacolor: { - key: 'headermetacolor', - transform: (v: string) => v.replace('#', ''), - skipIfEmpty: true, - }, + headermetacolor: { + key: 'headermetacolor', + transform: (v: string) => typeof v === 'string' ? v.replace('#', '') : '', + skipIfEmpty: true, + }, // Apply similar changes to other properties
337-345: 🛠️ Refactor suggestionEnsure parent directory exists before writing
case-model.cfgAdd a directory existence check before writing to the case model configuration file to prevent potential errors on first-boot systems.
-const modelToSet = path.basename(paths.caseModelTarget); // e.g., 'case-model.png' -await fs.writeFile(this.caseModelCfg, modelToSet); +const modelToSet = path.basename(paths.caseModelTarget); // e.g., 'case-model.png' +await fs.mkdir(path.dirname(this.caseModelCfg), { recursive: true }); +await fs.writeFile(this.caseModelCfg, modelToSet);
🧹 Nitpick comments (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)
170-176: Consider path validation for case icon pathWhile the method works correctly, it would be safer to validate the returned path to prevent potential path traversal issues.
public async getCaseIconWebguiPath(): Promise<string | null> { const paths = getters.paths(); if (await fileExists(paths.caseModelSource)) { + // Validate path is within expected bounds + if (!paths.caseModelTarget.startsWith('/usr/local/emhttp/')) { + this.logger.warn(`Invalid target path: ${paths.caseModelTarget}`); + return null; + } return convertWebGuiPathToAssetPath(paths.caseModelTarget); } return null; }
178-184: Consider path validation for partner logo pathSimilar to the case icon method, adding path validation would improve security.
public async getPartnerLogoWebguiPath(): Promise<string | null> { const paths = getters.paths(); if (await fileExists(paths.partnerLogoSource)) { + // Validate path is within expected bounds + if (!paths.partnerLogoTarget.startsWith('/usr/local/emhttp/')) { + this.logger.warn(`Invalid target path: ${paths.partnerLogoTarget}`); + return null; + } return convertWebGuiPathToAssetPath(paths.partnerLogoTarget); } return null; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
📒 Files selected for processing (12)
api/dev/activation/applied.txt(1 hunks)api/dev/dynamix/dynamix.cfg(1 hunks)api/src/unraid-api/auth/authentication.guard.ts(4 hunks)api/src/unraid-api/auth/public.decorator.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.service.ts(1 hunks)api/src/utils.ts(1 hunks)web/components/Activation/store/activationCodeModal.ts(1 hunks)web/pages/index.vue(3 hunks)web/pages/welcome.vue(1 hunks)
✅ Files skipped from review due to trivial changes (2)
- api/dev/activation/applied.txt
- api/dev/dynamix/dynamix.cfg
🚧 Files skipped from review as they are similar to previous changes (8)
- api/src/unraid-api/auth/public.decorator.ts
- api/src/unraid-api/auth/authentication.guard.ts
- web/pages/index.vue
- api/src/utils.ts
- web/pages/welcome.vue
- api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts
- web/components/Activation/store/activationCodeModal.ts
- api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts
⏰ Context from checks skipped due to timeout of 90000ms (2)
- GitHub Check: Test API
- GitHub Check: Cloudflare Pages
🔇 Additional comments (11)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (5)
31-40: Well-implemented first boot setup flag managementThis method correctly creates a directory (if missing) and manages a flag file to track first boot setup status. The return value provides useful context for the caller.
47-54: Good handling of missing configuration pathThe code correctly logs an error when the critical configuration path is missing and returns early to prevent undefined behavior in subsequent operations.
95-113: Robust method for finding activation JSON fileThis method handles all error cases properly - both when the activation directory doesn't exist and when there are issues reading its content.
137-168: Well-implemented activation data retrieval with cachingThe caching of activation data prevents redundant file reads and parsing, which is a nice performance optimization. The error handling is thorough and validation is properly implemented.
401-455: Well-implemented configuration file update utilityThe
updateCfgFilemethod is robust, handling file existence checks, section creation if needed, and proper merging of updated values. The error handling is thorough and the implementation preserves existing configuration.api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (6)
141-143: Good test cleanup with timer resetProper cleanup of timers after each test prevents potential timing issues between tests.
201-252: Comprehensive happy path testThis test thoroughly verifies the entire customization flow, including flag creation, activation data loading, and the application of all customization types. The assertions are detailed and cover all relevant aspects.
254-324: Thorough error handling testThis test effectively verifies that errors in one part of the customization process don't prevent other parts from being applied, which is a critical requirement for resilience.
393-411: Good test for hex color transformationThis test verifies that invalid hex colors are properly handled by the transformations, which is important for robustness.
413-429: Useful test for hex color normalizationThis test ensures that hex colors without the # prefix are correctly normalized, verifying important business logic in the service.
954-1107: Excellent standalone tests for updateCfgFileThese tests thoroughly verify the behavior of the private utility method, covering creation, updates, merging, and error handling scenarios.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)
115-124: OptimizegetPublicPartnerInfoby caching redundant function call.The method calls
getPartnerLogoWebguiPath()twice, which could lead to unnecessary file system operations.public async getPublicPartnerInfo(): Promise<PublicPartnerInfo | null> { const activationData = await this.getActivationData(); + const partnerLogoUrl = await this.getPartnerLogoWebguiPath(); return { - hasPartnerLogo: (await this.getPartnerLogoWebguiPath()) !== null, + hasPartnerLogo: partnerLogoUrl !== null, partnerName: activationData?.partnerName, partnerUrl: activationData?.partnerUrl, - partnerLogoUrl: await this.getPartnerLogoWebguiPath(), + partnerLogoUrl: partnerLogoUrl, }; }
266-283: Add type safety for activation data properties.The transform functions don't check input types, which could cause runtime errors if activation data doesn't match expected types.
const displayMappings: Record<string, DisplayMapping> = { - header: { key: 'header', transform: (v: string) => v.replace('#', ''), skipIfEmpty: true }, + header: { + key: 'header', + transform: (v: string) => typeof v === 'string' ? v.replace('#', '') : '', + skipIfEmpty: true + }, - headermetacolor: { - key: 'headermetacolor', - transform: (v: string) => v.replace('#', ''), - skipIfEmpty: true, - }, + headermetacolor: { + key: 'headermetacolor', + transform: (v: string) => typeof v === 'string' ? v.replace('#', '') : '', + skipIfEmpty: true, + }, - background: { - key: 'background', - transform: (v: string) => v.replace('#', ''), - skipIfEmpty: true, - }, + background: { + key: 'background', + transform: (v: string) => typeof v === 'string' ? v.replace('#', '') : '', + skipIfEmpty: true, + }, showBannerGradient: { key: 'showBannerGradient', - transform: (v: boolean) => (v ? 'yes' : 'no'), + transform: (v: boolean) => (typeof v === 'boolean' ? (v ? 'yes' : 'no') : 'no'), }, theme: { key: 'theme' }, };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
📒 Files selected for processing (1)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (4)
- GitHub Check: Build Web App
- GitHub Check: Build API
- GitHub Check: Cloudflare Pages
- GitHub Check: Test API
🔇 Additional comments (3)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (3)
170-176: Add path validation to prevent path traversal issues.The method should validate that the provided path is within expected bounds to prevent path traversal issues.
public async getCaseIconWebguiPath(): Promise<string | null> { const paths = getters.paths(); if (await fileExists(paths.caseModelSource)) { + // Validate path is within expected bounds + if (!paths.caseModelTarget.startsWith('/usr/local/emhttp/')) { + this.logger.warn(`Invalid target path: ${paths.caseModelTarget}`); + return null; + } return convertWebGuiPathToAssetPath(paths.caseModelTarget); } return null; }
178-184: Add path validation for partner logo paths.Similar to the case icon path, validate the path to prevent potential path traversal vulnerabilities.
public async getPartnerLogoWebguiPath(): Promise<string | null> { const paths = getters.paths(); if (await fileExists(paths.partnerLogoSource)) { + // Validate path is within expected bounds + if (!paths.partnerLogoTarget.startsWith('/usr/local/emhttp/')) { + this.logger.warn(`Invalid target path: ${paths.partnerLogoTarget}`); + return null; + } return convertWebGuiPathToAssetPath(paths.partnerLogoTarget); } return null; }
1-458: Code implementation looks comprehensive and well-structured.Overall, the
CustomizationServiceimplementation is thorough with good error handling, proper logging, and careful file operations. The service correctly handles initialization, activation data retrieval, customization application, and config file updates.
28f9c5e to
9e6e3c2
Compare
…ted dependencies in pnpm-lock.yaml
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (3)
web/components/Activation/ActivationPartnerLogoImg.vue (2)
16-21: Add alt text for accessibility.The image element is missing an alt attribute, which is important for accessibility. Even if the image is decorative, you should include an empty alt attribute.
<img v-if="partnerInfo?.partnerLogoUrl" :src="partnerInfo?.partnerLogoUrl" + :alt="partnerInfo?.partnerName || ''" class="w-72" :class="{ invert: darkMode && partnerInfo.hasPartnerLogo }" />
16-21: Add error handling for the partner logo image.Consider adding error handling for the image to gracefully handle cases where the image fails to load.
<img v-if="partnerInfo?.partnerLogoUrl" :src="partnerInfo?.partnerLogoUrl" class="w-72" :class="{ invert: darkMode && partnerInfo.hasPartnerLogo }" + @error="handleImageError" />With a corresponding method in the script section:
const handleImageError = (e) => { console.error('Failed to load partner logo image', e); // Optional: Set a flag to hide the image or show a fallback };web/pages/welcome.vue (1)
35-35: Misleading label in debug panel.The label "Manually Hidden" is displaying the
isVisiblevalue, which is confusing since they represent opposite states. The label suggests it should show whether the modal is hidden, but it's showing if it's visible.- <li>Manually Hidden (`activationModalHidden`): {{ isVisible }}</li> + <li>Is Visible (`isVisible`): {{ isVisible }}</li>
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
📒 Files selected for processing (5)
api/dev/Unraid.net/myservers.cfg(1 hunks)api/dev/activation/activation_code_12345.activationcode(1 hunks)api/dev/dynamix/dynamix.cfg(1 hunks)web/components/Activation/ActivationPartnerLogoImg.vue(1 hunks)web/pages/welcome.vue(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- api/dev/Unraid.net/myservers.cfg
🚧 Files skipped from review as they are similar to previous changes (2)
- api/dev/activation/activation_code_12345.activationcode
- api/dev/dynamix/dynamix.cfg
🧰 Additional context used
🧠 Learnings (1)
web/pages/welcome.vue (1)
Learnt from: elibosley
PR: unraid/api#1369
File: web/pages/welcome.vue:29-43
Timestamp: 2025-04-28T17:28:19.796Z
Learning: The pages in the web/pages/ directory are debug views only and not published to production. Debug panels and console logs are acceptable in these views.
⏰ Context from checks skipped due to timeout of 90000ms (4)
- GitHub Check: Build API
- GitHub Check: Build Web App
- GitHub Check: Test API
- GitHub Check: Cloudflare Pages
🔇 Additional comments (5)
web/components/Activation/ActivationPartnerLogoImg.vue (1)
1-13: Great component structure and implementation.The component is well structured using Vue 3 Composition API and properly leverages Pinia stores for reactive state management. The documentation about the black image requirement for dark mode inversion is helpful for future maintainers.
web/pages/welcome.vue (4)
2-11: Good refactoring to Pinia stores.The refactoring from server state to Pinia stores is well-implemented. The imports and store references are properly structured, and the use of
storeToRefsensures reactivity while preventing unnecessary re-renders.
13-20: Clear and effective modal control function.The
showActivationModalfunction is well-documented and properly implements the override for the default modal visibility logic.
27-28: Successful integration of new components.The removal of server state props from the
WelcomeModalCecomponent and the addition of the newModalsCecomponent aligns well with the broader refactoring to API-driven activation code data.
29-43: Appropriate debug panel for development view.The debug panel provides useful information for development and testing. Based on the retrieved learning, this is acceptable since pages in the
web/pages/directory are debug views only and not published to production.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
api/src/unraid-api/unraid-file-modifier/unraid-file-modifier.service.ts (1)
45-72: Good implementation of flexible module loading.The implementation supports both default exports and named exports, making it more accommodating to different coding styles. The approach properly filters non-class exports and validates that discovered classes extend
FileModification.Consider adding a warning if multiple eligible classes are found in a single file, as currently only the first one is used:
if (exportedKeys.length > 0) { const firstExportKey = exportedKeys[0]; + if (exportedKeys.length > 1) { + this.logger.warn(`Multiple exported classes found in ${path}, using ${firstExportKey}`); + } const ExportedClass = module[firstExportKey];
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)
📒 Files selected for processing (1)
api/src/unraid-api/unraid-file-modifier/unraid-file-modifier.service.ts(2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (4)
- GitHub Check: Build Web App
- GitHub Check: Build API
- GitHub Check: Test API
- GitHub Check: Cloudflare Pages
🔇 Additional comments (4)
api/src/unraid-api/unraid-file-modifier/unraid-file-modifier.service.ts (4)
39-42: Dynamic file loading improves modularity.The implementation of dynamic module loading using
import.meta.globis a significant improvement over manual imports. This approach automatically discovers all modification files following the*.modification.tspattern, making the system more maintainable and extensible.
44-44: Helpful debug logging.Adding debug logging for the number of discovered modifications improves observability and helps with troubleshooting.
104-104: Improved error logging.Using the
%oformat specifier for error objects provides more detailed error information, which is helpful for debugging.
37-75:Details
✅ Verification successful
Verify new modifications are loaded correctly.
The dynamic loading mechanism assumes a specific file naming pattern and export structure. Make sure all modification files follow this pattern and test that they're properly discovered and instantiated.
🏁 Script executed:
#!/bin/bash # Check for all modification files and their export patterns echo "Finding all modification files:" find api/src/unraid-api/unraid-file-modifier/modifications -name "*.modification.ts" | sort echo "\nExamining export patterns in modification files:" for file in $(find api/src/unraid-api/unraid-file-modifier/modifications -name "*.modification.ts"); do echo "\n$file exports:" grep -E "export (default|class)" $file doneLength of output: 5596
All modifications follow the expected export pattern and are loaded correctly
Inspection of the nine
*.modification.tsfiles underapi/src/unraid-api/unraid-file-modifier/modificationsshows eight usingexport default class …and one (PartnerLogoCopierModification) using a namedexport class …. The loader’s logic covers both cases—loading the default export first, then falling back to the named export—so all modules will be discovered and instantiated as intended.
|
This plugin has been deployed to Cloudflare R2 and is available for testing. |


Summary by CodeRabbit