Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/create-docusaurus-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,26 @@ jobs:
echo "Source directory does not exist!"
exit 1
fi
# Remove old API docs but preserve other folders
rm -rf docs-repo/docs/API/
mkdir -p docs-repo/docs/API

# Copy all markdown files and maintain directory structure
cp -r source-repo/api/docs/public/. docs-repo/docs/API/

# Clean and copy images directory specifically
rm -rf docs-repo/docs/API/images/
mkdir -p docs-repo/docs/API/images

# Copy images from public/images if they exist
if [ -d "source-repo/api/docs/public/images" ]; then
cp -r source-repo/api/docs/public/images/. docs-repo/docs/API/images/
fi

# Also copy any images from the parent docs/images directory
if [ -d "source-repo/api/docs/images" ]; then
cp -r source-repo/api/docs/images/. docs-repo/docs/API/images/
fi
Comment on lines +40 to +59
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix trailing whitespaces to address YAMLlint errors.

The static analysis tool detected trailing spaces on multiple lines that should be cleaned up for consistency.

Apply this diff to remove trailing whitespace:

-          # Remove old API docs but preserve other folders
+          # Remove old API docs but preserve other folders
           rm -rf docs-repo/docs/API/
           mkdir -p docs-repo/docs/API
-          
+          
-          # Copy all markdown files and maintain directory structure
+          # Copy all markdown files and maintain directory structure
           cp -r source-repo/api/docs/public/. docs-repo/docs/API/
-          
+          
-          # Clean and copy images directory specifically
+          # Clean and copy images directory specifically
           rm -rf docs-repo/docs/API/images/
           mkdir -p docs-repo/docs/API/images
-          
+          
-          # Copy images from public/images if they exist
+          # Copy images from public/images if they exist
           if [ -d "source-repo/api/docs/public/images" ]; then
             cp -r source-repo/api/docs/public/images/. docs-repo/docs/API/images/
           fi
-          
+          
-          # Also copy any images from the parent docs/images directory
+          # Also copy any images from the parent docs/images directory
           if [ -d "source-repo/api/docs/images" ]; then
             cp -r source-repo/api/docs/images/. docs-repo/docs/API/images/
           fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Remove old API docs but preserve other folders
rm -rf docs-repo/docs/API/
mkdir -p docs-repo/docs/API
# Copy all markdown files and maintain directory structure
cp -r source-repo/api/docs/public/. docs-repo/docs/API/
# Clean and copy images directory specifically
rm -rf docs-repo/docs/API/images/
mkdir -p docs-repo/docs/API/images
# Copy images from public/images if they exist
if [ -d "source-repo/api/docs/public/images" ]; then
cp -r source-repo/api/docs/public/images/. docs-repo/docs/API/images/
fi
# Also copy any images from the parent docs/images directory
if [ -d "source-repo/api/docs/images" ]; then
cp -r source-repo/api/docs/images/. docs-repo/docs/API/images/
fi
# Remove old API docs but preserve other folders
rm -rf docs-repo/docs/API/
mkdir -p docs-repo/docs/API
# Copy all markdown files and maintain directory structure
cp -r source-repo/api/docs/public/. docs-repo/docs/API/
# Clean and copy images directory specifically
rm -rf docs-repo/docs/API/images/
mkdir -p docs-repo/docs/API/images
# Copy images from public/images if they exist
if [ -d "source-repo/api/docs/public/images" ]; then
cp -r source-repo/api/docs/public/images/. docs-repo/docs/API/images/
fi
# Also copy any images from the parent docs/images directory
if [ -d "source-repo/api/docs/images" ]; then
cp -r source-repo/api/docs/images/. docs-repo/docs/API/images/
fi
🧰 Tools
🪛 YAMLlint (1.37.1)

[error] 43-43: trailing spaces

(trailing-spaces)


[error] 46-46: trailing spaces

(trailing-spaces)


[error] 50-50: trailing spaces

(trailing-spaces)


[error] 55-55: trailing spaces

(trailing-spaces)

🤖 Prompt for AI Agents
.github/workflows/create-docusaurus-pr.yml around lines 40 to 59: several lines
contain trailing whitespace triggering YAMLlint errors; remove all trailing
spaces at the ends of lines in this block (including blank lines), ensure
indentation and line endings remain unchanged, and re-run the linter (or git
diff) to confirm no trailing whitespace remains before committing.

- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
Expand Down
20 changes: 15 additions & 5 deletions api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1512,20 +1512,30 @@ describe('OidcAuthService', () => {
describe('getRedirectUri (private method)', () => {
it('should generate correct redirect URI with localhost (development)', () => {
const getRedirectUri = (service as any).getRedirectUri.bind(service);
const redirectUri = getRedirectUri('localhost:3001');
const redirectUri = getRedirectUri('http://localhost:3000');

expect(redirectUri).toBe('http://localhost:3000/graphql/api/auth/oidc/callback');
});

it('should generate correct redirect URI with non-localhost host', () => {
const getRedirectUri = (service as any).getRedirectUri.bind(service);
const redirectUri = getRedirectUri('https://example.com');

// Mock the ConfigService to return a production base URL
configService.get.mockReturnValue('https://example.com');
expect(redirectUri).toBe('https://example.com/graphql/api/auth/oidc/callback');
});

const redirectUri = getRedirectUri('example.com:443');
it('should handle HTTP protocol for non-localhost hosts', () => {
const getRedirectUri = (service as any).getRedirectUri.bind(service);
const redirectUri = getRedirectUri('http://tower.local');

expect(redirectUri).toBe('https://example.com/graphql/api/auth/oidc/callback');
expect(redirectUri).toBe('http://tower.local/graphql/api/auth/oidc/callback');
});

it('should handle non-standard ports correctly', () => {
const getRedirectUri = (service as any).getRedirectUri.bind(service);
const redirectUri = getRedirectUri('http://example.com:8080');

expect(redirectUri).toBe('http://example.com:8080/graphql/api/auth/oidc/callback');
});

it('should use default redirect URI when no request host provided', () => {
Expand Down
59 changes: 34 additions & 25 deletions api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,17 @@ export class OidcAuthService {
private readonly validationService: OidcValidationService
) {}

async getAuthorizationUrl(providerId: string, state: string, requestHost?: string): Promise<string> {
async getAuthorizationUrl(
providerId: string,
state: string,
requestOrigin?: string
): Promise<string> {
const provider = await this.oidcConfig.getProvider(providerId);
if (!provider) {
throw new UnauthorizedException(`Provider ${providerId} not found`);
}

const redirectUri = this.getRedirectUri(requestHost);
const redirectUri = this.getRedirectUri(requestOrigin);

// Generate secure state with cryptographic signature
const secureState = this.stateService.generateSecureState(providerId, state);
Expand Down Expand Up @@ -110,7 +114,7 @@ export class OidcAuthService {
providerId: string,
code: string,
state: string,
requestHost?: string,
requestOrigin?: string,
fullCallbackUrl?: string
): Promise<string> {
const provider = await this.oidcConfig.getProvider(providerId);
Expand All @@ -119,7 +123,7 @@ export class OidcAuthService {
}

try {
const redirectUri = this.getRedirectUri(requestHost);
const redirectUri = this.getRedirectUri(requestOrigin);

// Always use openid-client for consistency
const config = await this.getOrCreateConfig(provider);
Expand Down Expand Up @@ -634,30 +638,35 @@ export class OidcAuthService {
return this.validationService.validateProvider(provider);
}

private getRedirectUri(requestHost?: string): string {
// Always use the proxied path through /graphql to match production
if (requestHost && requestHost.includes('localhost')) {
// In development, use the Nuxt proxy at port 3000
return `http://localhost:3000/graphql/api/auth/oidc/callback`;
}
private getRedirectUri(requestOrigin?: string): string {
// If we have the full origin (protocol://host), use it directly
if (requestOrigin) {
// Parse the origin to extract protocol and host
try {
const url = new URL(requestOrigin);
const { protocol, hostname, port } = url;

// Reconstruct the URL, removing default ports
let cleanOrigin = `${protocol}//${hostname}`;

// Add port if it's not the default for the protocol
if (
port &&
!(protocol === 'https:' && port === '443') &&
!(protocol === 'http:' && port === '80')
) {
cleanOrigin += `:${port}`;
}

// In production, use the actual request host or configured base URL
if (requestHost) {
// Parse the host to handle port numbers properly
const isLocalhost = requestHost.includes('localhost');
const protocol = isLocalhost ? 'http' : 'https';

// Remove standard ports (:443 for HTTPS, :80 for HTTP)
let cleanHost = requestHost;
if (!isLocalhost) {
if (requestHost.endsWith(':443')) {
cleanHost = requestHost.slice(0, -4); // Remove :443
} else if (requestHost.endsWith(':80')) {
cleanHost = requestHost.slice(0, -3); // Remove :80
// Special handling for localhost development with Nuxt proxy
if (hostname === 'localhost' && port === '3000') {
return `${cleanOrigin}/graphql/api/auth/oidc/callback`;
}
}

return `${protocol}://${cleanHost}/graphql/api/auth/oidc/callback`;
return `${cleanOrigin}/graphql/api/auth/oidc/callback`;
} catch (e) {
this.logger.warn(`Failed to parse request origin: ${requestOrigin}, error: ${e}`);
}
}

// Fall back to configured BASE_URL or default
Expand Down
15 changes: 11 additions & 4 deletions api/src/unraid-api/rest/rest.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,16 @@ export class RestController {
return res.status(400).send('State parameter is required');
}

// Get the host from the request headers
const host = req.headers.host || undefined;
// Get the host and protocol from the request headers
const protocol = (req.headers['x-forwarded-proto'] as string) || req.protocol || 'http';
const host = (req.headers['x-forwarded-host'] as string) || req.headers.host || undefined;
const requestInfo = host ? `${protocol}://${host}` : undefined;

const authUrl = await this.oidcAuthService.getAuthorizationUrl(providerId, state, host);
const authUrl = await this.oidcAuthService.getAuthorizationUrl(
providerId,
state,
requestInfo
);
this.logger.log(`Redirecting to OIDC provider: ${authUrl}`);

// Manually set redirect headers for better proxy compatibility
Expand Down Expand Up @@ -125,14 +131,15 @@ export class RestController {
const host =
(req.headers['x-forwarded-host'] as string) || req.headers.host || 'localhost:3000';
const fullUrl = `${protocol}://${host}${req.url}`;
const requestInfo = `${protocol}://${host}`;

this.logger.debug(`Full callback URL from request: ${fullUrl}`);

const paddedToken = await this.oidcAuthService.handleCallback(
providerId,
code,
state,
host,
requestInfo,
fullUrl
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ function verifyUsernamePasswordAndSSO(string $username, string $password): bool
return true;
}
// We may have an SSO token, attempt validation
if (strlen($password) > 800) {
if (strlen($password) > 500) {
if (!preg_match('/^[A-Za-z0-9-_]+.[A-Za-z0-9-_]+.[A-Za-z0-9-_]+$/', $password)) {
my_logger("SSO Login Attempt Failed: Invalid token format");
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/.login.php
+ return true;
+ }
+ // We may have an SSO token, attempt validation
+ if (strlen($password) > 800) {
+ if (strlen($password) > 500) {
+ if (!preg_match('/^[A-Za-z0-9-_]+.[A-Za-z0-9-_]+.[A-Za-z0-9-_]+$/', $password)) {
+ my_logger("SSO Login Attempt Failed: Invalid token format");
+ return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function verifyUsernamePasswordAndSSO(string $username, string $password): bool
return true;
}
// We may have an SSO token, attempt validation
if (strlen($password) > 800) {
if (strlen($password) > 500) {
if (!preg_match('/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/', $password)) {
my_logger("SSO Login Attempt Failed: Invalid token format");
return false;
Expand Down
Loading