mirror of
https://github.com/unraid/api.git
synced 2026-01-02 06:30:02 -06:00
Compare commits
5 Commits
v4.26.2
...
4.13.0-bui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86b4e2cb40 | ||
|
|
e5b71c6a49 | ||
|
|
0eb7127057 | ||
|
|
75030611fe | ||
|
|
216dc75d66 |
17
.github/workflows/create-docusaurus-pr.yml
vendored
17
.github/workflows/create-docusaurus-pr.yml
vendored
@@ -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
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
|
||||
@@ -1512,22 +1512,32 @@ 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);
|
||||
|
||||
// Mock the ConfigService to return a production base URL
|
||||
configService.get.mockReturnValue('https://example.com');
|
||||
|
||||
const redirectUri = getRedirectUri('example.com:443');
|
||||
const redirectUri = getRedirectUri('https://example.com');
|
||||
|
||||
expect(redirectUri).toBe('https://example.com/graphql/api/auth/oidc/callback');
|
||||
});
|
||||
|
||||
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('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', () => {
|
||||
const getRedirectUri = (service as any).getRedirectUri.bind(service);
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
// 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';
|
||||
// Reconstruct the URL, removing default ports
|
||||
let cleanOrigin = `${protocol}//${hostname}`;
|
||||
|
||||
// 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
|
||||
// Add port if it's not the default for the protocol
|
||||
if (
|
||||
port &&
|
||||
!(protocol === 'https:' && port === '443') &&
|
||||
!(protocol === 'http:' && port === '80')
|
||||
) {
|
||||
cleanOrigin += `:${port}`;
|
||||
}
|
||||
}
|
||||
|
||||
return `${protocol}://${cleanHost}/graphql/api/auth/oidc/callback`;
|
||||
// Special handling for localhost development with Nuxt proxy
|
||||
if (hostname === 'localhost' && port === '3000') {
|
||||
return `${cleanOrigin}/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
|
||||
|
||||
@@ -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
|
||||
@@ -125,6 +131,7 @@ 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}`);
|
||||
|
||||
@@ -132,7 +139,7 @@ export class RestController {
|
||||
providerId,
|
||||
code,
|
||||
state,
|
||||
host,
|
||||
requestInfo,
|
||||
fullUrl
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user