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
116 changes: 116 additions & 0 deletions src/main/frontend/styles/oauth-callback.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
@import url('./base.css');

:root {
--font-body: "Space Mono", "IBM Plex Mono", monospace;
--font-display: "Archivo Black", "Arial Black", sans-serif;
--bg-primary: #f5f5f0;
--bg-card: #ffffff;
--text-primary: #0a0a0a;
--text-secondary: #666666;
--text-inverse: #ffffff;
--border-default: #0a0a0a;
--accent: #ff3300;
}

:root[data-theme="dark"] {
--accent: #ff3300;
--bg-primary: #0a0a0a;
--bg-card: #1a1a1a;
--text-primary: #f5f5f0;
--text-secondary: #a3a3a3;
--text-inverse: #0a0a0a;
--border-default: #f5f5f0;
}

@media (prefers-color-scheme: dark) {
:root:not([data-theme]) {
--accent: #ff3300;
--bg-primary: #0a0a0a;
--bg-card: #1a1a1a;
--text-primary: #f5f5f0;
--text-secondary: #a3a3a3;
--text-inverse: #0a0a0a;
--border-default: #f5f5f0;
}
}

body {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: 24px;
}

.callback-page {
width: min(100%, 480px);
padding: 32px;
border: 3px solid var(--border-default);
border-top-color: var(--accent);
background: var(--bg-card);
box-shadow: 6px 6px 0 var(--border-default);
overflow-wrap: anywhere;
}

.callback-brand {
margin-bottom: 32px;
font-family: var(--font-display);
font-size: 14px;
text-transform: uppercase;
}

h1 {
margin-bottom: 16px;
font-size: 28px;
line-height: 1.2;
letter-spacing: -0.02em;
text-wrap: balance;
}

.callback-message {
line-height: 1.6;
text-wrap: pretty;
}

.callback-hint {
margin-top: 12px;
font-size: 14px;
line-height: 1.6;
color: var(--text-secondary);
}

.callback-action {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-height: 48px;
margin-top: 28px;
padding: 12px 16px;
border: 2px solid var(--border-default);
background: var(--text-primary);
color: var(--text-inverse);
font-size: 14px;
font-weight: 700;
text-decoration: none;
}

.callback-action:hover {
background: var(--bg-card);
color: var(--text-primary);
}

.callback-action:focus-visible {
outline: 3px solid var(--text-primary);
outline-offset: 4px;
}

@media (max-width: 380px) {
.callback-page {
padding: 24px 20px;
}

h1 {
font-size: 24px;
}
}
114 changes: 114 additions & 0 deletions src/main/java/Server/http/OAuthCallbackPage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package Server.http;

import com.sun.net.httpserver.HttpExchange;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;

/** Renders the return page for Spotify and Discogs login. */
public final class OAuthCallbackPage {
private OAuthCallbackPage() {}

public enum Provider {
SPOTIFY("Spotify", "spotify-auth-callback", "/"),
DISCOGS("Discogs", "discogs-auth-callback", "/playlist.html");

private final String name;
private final String eventType;
private final String returnPath;

Provider(String name, String eventType, String returnPath) {
this.name = name;
this.eventType = eventType;
this.returnPath = returnPath;
}
}

public static void send(HttpExchange exchange, Provider provider, boolean success, String message) throws IOException {
byte[] body = render(provider, success, message).getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "text/html; charset=utf-8");
exchange.getResponseHeaders().set("Cache-Control", "no-store");
exchange.sendResponseHeaders(200, body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(body);
}
}

public static String render(Provider provider, boolean success, String message) {
String title = provider.name + (success ? " connected" : " connection failed");
return """
<!DOCTYPE html>
<html lang="en" data-callback-success="%s" data-callback-type="%s"
data-callback-target="%s" data-callback-message="%s">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VinylMatch - %s</title>
<script>
(function() {
var root = document.documentElement;
try {
var theme = localStorage.getItem('vinylmatch_theme');
if (theme === 'light' || theme === 'dark') root.dataset.theme = theme;
} catch (_) {}

var success = root.dataset.callbackSuccess === 'true';
var payload = { type: root.dataset.callbackType, success: success };
if (!success && root.dataset.callbackMessage) {
payload.message = root.dataset.callbackMessage;
}
if (window.opener && !window.opener.closed) {
try {
window.opener.postMessage(payload, window.location.origin);
} catch (error) {
console.error('Could not notify VinylMatch of the login result.', error);
}
}

function returnToApp() {
window.close();
if (!window.closed) {
window.location.replace(root.dataset.callbackTarget);
}
}

if (success) {
returnToApp();
}
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('oauth-callback-action').addEventListener('click', function(event) {
event.preventDefault();
returnToApp();
});
});
}());
</script>
<link rel="stylesheet" href="/styles/oauth-callback.css">
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
</head>
<body>
<main class="callback-page" aria-labelledby="callback-title">
<p class="callback-brand">Vinyl Match</p>
<h1 id="callback-title">%s</h1>
<p class="callback-message">%s</p>
<p class="callback-hint">%s</p>
<a id="oauth-callback-action" class="callback-action" href="%s">Return to VinylMatch <span aria-hidden="true">&rarr;</span></a>
</main>
</body>
</html>
""".formatted(success, provider.eventType, provider.returnPath, escapeHtml(message), title,
title, success ? "Returning to your music." : escapeHtml(message),
success ? "If this window stays open, use the link below."
: "Return to the app to try again.", provider.returnPath);
}

private static String escapeHtml(String value) {
if (value == null) return "";
return value.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#39;");
}
}
Loading
Loading