Skip to content
Draft
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
8 changes: 4 additions & 4 deletions Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,17 @@
# Prevent MIME type sniffing
X-Content-Type-Options "nosniff"

# Clickjacking protection
X-Frame-Options "SAMEORIGIN"
# X-Frame-Options removed - frame-ancestors in CSP (set by app) provides better control
# See src/Configuration/StartupExtensions.cs for CSP configuration

# XSS protection (legacy browsers)
X-XSS-Protection "1; mode=block"

# Referrer policy
Referrer-Policy "strict-origin-when-cross-origin"

# Content Security Policy
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self';"
# Content Security Policy is handled by the application middleware
# CSP headers are set in src/Configuration/StartupExtensions.cs with nonce support

# Permissions policy - Deny access to sensitive browser features
Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()"
Expand Down
11 changes: 7 additions & 4 deletions src/Configuration/StartupExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,22 +195,25 @@ this WebApplicationBuilder builder
_ = app.Use( async ( ctx, next ) => {
string path = ctx.Request.Path.Value ?? string.Empty;
bool isEmbed = path.EndsWith( "/embed", StringComparison.OrdinalIgnoreCase ) && (path.StartsWith( "/card/", StringComparison.OrdinalIgnoreCase ) || path.StartsWith( "/playlist/", StringComparison.OrdinalIgnoreCase ));
bool isAppleMusic = path.StartsWith( "/applemusic", StringComparison.OrdinalIgnoreCase );

// Generate a unique nonce for this request to allow inline scripts and styles
string nonce = Convert.ToBase64String( System.Security.Cryptography.RandomNumberGenerator.GetBytes( 16 ) );
ctx.Items["CSPNonce"] = nonce;

// Build CSP policy
// Allow MusicKit JS CDN, nonce-based inline styles, and API calls to music providers
string frameAncestors = isEmbed ? "*" : "'self'"; // Allow any site to embed cards & playlists
// Allow MusicKit JS CDN, Cloudflare analytics, nonce-based inline styles, and API calls to music providers
// Allow framing for embed endpoints, Apple Music integration, and playlist pages
// Note: frame-ancestors uses https: scheme with 'self' to support HTTPS framing including same-origin
string frameAncestors = (isEmbed || isAppleMusic) ? "https: 'self'" : "'self'";

string csp = string.Join( "; ", new[] {
"default-src 'self'",
$"script-src 'self' 'nonce-{nonce}' https://js-cdn.music.apple.com",
$"script-src 'self' 'nonce-{nonce}' https://js-cdn.music.apple.com https://static.cloudflareinsights.com",
$"style-src 'self' 'nonce-{nonce}'",
"img-src 'self' data: https:",
"font-src 'self' data:",
"connect-src 'self' https://api.music.apple.com https://accounts.spotify.com https://api.spotify.com https://openapi.tidal.com",
"connect-src 'self' https://api.music.apple.com https://accounts.spotify.com https://api.spotify.com https://openapi.tidal.com https://cloudflareinsights.com",
"media-src 'self' https:",
"frame-ancestors " + frameAncestors,
"object-src 'none'",
Expand Down
1 change: 1 addition & 0 deletions src/Web/Views/Shared/_Layout.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<title>@title - TuneBridge</title>
<link rel="stylesheet" href="~/public/docfx.min.css">
<link rel="stylesheet" href="~/public/main.css">
<link rel="stylesheet" href="~/public/playlist-manager.css">
<link rel="stylesheet" href="~/public/TuneBridge.styles.css">
<meta name="loc:themeLight" content="Light">
<meta name="loc:themeDark" content="Dark">
Expand Down
141 changes: 141 additions & 0 deletions src/Web/wwwroot/public/playlist-manager.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/* Playlist Management Styles - v1.0 */

.playlist-toolbar {
position: sticky;
top: 0;
z-index: 1000;
background: var(--bs-body-bg);
border: 1px solid var(--bs-border-color);
padding: 1rem;
margin-bottom: 1rem;
border-radius: 0.375rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.playlist-toolbar-content {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}

.playlist-selection-info {
font-size: 1rem;
font-weight: 600;
color: var(--bs-body-color);
min-width: 120px;
}

.playlist-inputs {
flex: 1;
min-width: 200px;
}

.playlist-inputs input,
.playlist-inputs textarea {
width: 100%;
}

.playlist-actions {
display: flex;
gap: 0.5rem;
}

.card-selection-checkbox {
position: absolute;
top: 0.5rem;
left: 0.5rem;
z-index: 10;
}

.card-selection-checkbox input[type="checkbox"] {
appearance: none;
width: 24px;
height: 24px;
border: 2px solid var(--bs-primary);
border-radius: 4px;
background: var(--bs-body-bg);
cursor: pointer;
position: relative;
}

.card-selection-checkbox input[type="checkbox"]:checked {
background: var(--bs-primary);
}

.card-selection-checkbox input[type="checkbox"]:checked::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 16px;
font-weight: bold;
}

.card-selection-checkbox label {
position: absolute;
top: 0;
left: 0;
width: 24px;
height: 24px;
cursor: pointer;
}

.playlist-selection-mode .embed-card {
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
position: relative;
}

.playlist-selection-mode .embed-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(var(--bs-primary-rgb), 0.3);
}

.playlist-selection-mode .embed-card.selected {
border: 2px solid var(--bs-primary);
box-shadow: 0 0 0 3px rgba(var(--bs-primary-rgb), 0.2);
}

/* Adjust share button position when in selection mode */
.playlist-selection-mode .embed-header-top {
padding-left: 40px;
}

#startPlaylistBtn {
margin-top: 0.5rem;
}

@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}

.shake-animation {
animation: shake 0.3s ease-in-out;
}

@media (max-width: 768px) {
.playlist-toolbar-content {
flex-direction: column;
align-items: stretch;
}

.playlist-inputs {
order: 1;
min-width: 100%;
}

.playlist-selection-info {
order: 2;
}

.playlist-actions {
order: 3;
justify-content: space-between;
}
}
148 changes: 0 additions & 148 deletions src/Web/wwwroot/public/playlist-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -407,154 +407,6 @@

// Initialize on page load
function init() {
// Add styles
if (!document.getElementById('playlistStyles')) {
const style = document.createElement('style');
style.id = 'playlistStyles';
style.textContent = `
.playlist-toolbar {
position: sticky;
top: 0;
z-index: 1000;
background: var(--bs-body-bg);
border: 1px solid var(--bs-border-color);
padding: 1rem;
margin-bottom: 1rem;
border-radius: 0.375rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.playlist-toolbar-content {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}

.playlist-selection-info {
font-size: 1rem;
font-weight: 600;
color: var(--bs-body-color);
min-width: 120px;
}

.playlist-inputs {
flex: 1;
min-width: 200px;
}

.playlist-inputs input,
.playlist-inputs textarea {
width: 100%;
}

.playlist-actions {
display: flex;
gap: 0.5rem;
}

.card-selection-checkbox {
position: absolute;
top: 0.5rem;
left: 0.5rem;
z-index: 10;
}

.card-selection-checkbox input[type="checkbox"] {
appearance: none;
width: 24px;
height: 24px;
border: 2px solid var(--bs-primary);
border-radius: 4px;
background: var(--bs-body-bg);
cursor: pointer;
position: relative;
}

.card-selection-checkbox input[type="checkbox"]:checked {
background: var(--bs-primary);
}

.card-selection-checkbox input[type="checkbox"]:checked::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 16px;
font-weight: bold;
}

.card-selection-checkbox label {
position: absolute;
top: 0;
left: 0;
width: 24px;
height: 24px;
cursor: pointer;
}

.playlist-selection-mode .embed-card {
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
position: relative;
}

.playlist-selection-mode .embed-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(var(--bs-primary-rgb), 0.3);
}

.playlist-selection-mode .embed-card.selected {
border: 2px solid var(--bs-primary);
box-shadow: 0 0 0 3px rgba(var(--bs-primary-rgb), 0.2);
}

/* Adjust share button position when in selection mode */
.playlist-selection-mode .embed-header-top {
padding-left: 40px;
}

#startPlaylistBtn {
margin-top: 0.5rem;
}

@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}

.shake-animation {
animation: shake 0.3s ease-in-out;
}

@media (max-width: 768px) {
.playlist-toolbar-content {
flex-direction: column;
align-items: stretch;
}

.playlist-inputs {
order: 1;
min-width: 100%;
}

.playlist-selection-info {
order: 2;
}

.playlist-actions {
order: 3;
justify-content: space-between;
}
}
`;
document.head.appendChild(style);
}

// Add playlist button when results are loaded
// Use MutationObserver to detect when results are added
const observer = new MutationObserver((mutations) => {
Expand Down
Loading