460 lines
15 KiB
Go
460 lines
15 KiB
Go
package main
|
|
|
|
import (
|
|
"html/template"
|
|
"os"
|
|
)
|
|
|
|
func LoadTemplate() (*template.Template, error) {
|
|
// Try to load from file first
|
|
if _, err := os.Stat("template.html"); err == nil {
|
|
logger.Info("Loading template from template.html")
|
|
return template.ParseFiles("template.html")
|
|
}
|
|
|
|
// Fall back to embedded template
|
|
logger.Info("Using embedded template")
|
|
return template.New("page").Parse(embeddedTemplate)
|
|
}
|
|
|
|
func LoadAppTemplate() (*template.Template, error) {
|
|
// Try to load from file first
|
|
if _, err := os.Stat("app.html"); err == nil {
|
|
logger.Info("Loading app template from app.html")
|
|
return template.ParseFiles("app.html")
|
|
}
|
|
|
|
// Fall back to embedded template
|
|
logger.Info("Using embedded app template")
|
|
return template.New("app").Parse(embeddedAppTemplate)
|
|
}
|
|
|
|
const embeddedTemplate = `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Two-Step Authentication</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
|
|
background: linear-gradient(135deg, #0f0f1e 0%, #1a1a2e 50%, #16213e 100%);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #b8c5d6;
|
|
}
|
|
.container {
|
|
text-align: center;
|
|
width: 100%;
|
|
max-width: 500px;
|
|
padding: 20px;
|
|
}
|
|
h1 {
|
|
font-size: 28px;
|
|
margin-bottom: 40px;
|
|
color: #4a9eff;
|
|
font-weight: 300;
|
|
letter-spacing: 1px;
|
|
}
|
|
.form-group {
|
|
display: flex;
|
|
gap: 15px;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
input {
|
|
flex: 1;
|
|
max-width: 300px;
|
|
padding: 18px 24px;
|
|
font-size: 18px;
|
|
border: 2px solid #2c3e50;
|
|
background: #1e2835;
|
|
color: #e0e6ed;
|
|
border-radius: 8px;
|
|
outline: none;
|
|
transition: all 0.3s;
|
|
}
|
|
input:focus {
|
|
border-color: #4a9eff;
|
|
background: #252f3f;
|
|
box-shadow: 0 0 20px rgba(74, 158, 255, 0.2);
|
|
}
|
|
input::placeholder { color: #5a6c7d; }
|
|
button {
|
|
padding: 18px 32px;
|
|
font-size: 18px;
|
|
background: linear-gradient(135deg, #1e40af 0%, #3b82f6 100%);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
font-weight: 500;
|
|
}
|
|
button:hover {
|
|
background: linear-gradient(135deg, #2563eb 0%, #60a5fa 100%);
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 20px rgba(37, 99, 235, 0.3);
|
|
}
|
|
button:active { transform: translateY(0); }
|
|
.error {
|
|
color: #ef4444;
|
|
margin-top: 20px;
|
|
font-size: 16px;
|
|
background: rgba(239, 68, 68, 0.1);
|
|
padding: 12px 20px;
|
|
border-radius: 6px;
|
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
}
|
|
.success {
|
|
margin-top: 30px;
|
|
padding: 20px;
|
|
background: rgba(34, 197, 94, 0.1);
|
|
border: 2px solid #22c55e;
|
|
border-radius: 8px;
|
|
font-size: 18px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
{{if .Step2}}
|
|
<h1>Enter TOTP Code</h1>
|
|
<form method="POST" action="/verify-totp">
|
|
<div class="form-group">
|
|
<input type="text" name="totp" placeholder="000000" autofocus required pattern="[0-9]{6}" maxlength="6">
|
|
<button type="submit">Verify</button>
|
|
</div>
|
|
</form>
|
|
{{else}}
|
|
<h1>Enter User ID</h1>
|
|
<form method="POST" action="/verify-user">
|
|
<div class="form-group">
|
|
<input type="text" name="userid" placeholder="User ID" autofocus required>
|
|
<button type="submit">Continue</button>
|
|
</div>
|
|
</form>
|
|
{{end}}
|
|
{{if .Error}}<div class="error">{{.Error}}</div>{{end}}
|
|
</div>
|
|
</body>
|
|
</html>`
|
|
|
|
const embeddedAppTemplate = `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>REST API Client</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
|
|
background: linear-gradient(135deg, #0f0f1e 0%, #1a1a2e 50%, #16213e 100%);
|
|
min-height: 100vh;
|
|
color: #b8c5d6;
|
|
padding: 20px;
|
|
}
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
.header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 30px;
|
|
padding-bottom: 20px;
|
|
border-bottom: 2px solid #2c3e50;
|
|
}
|
|
h1 {
|
|
font-size: 28px;
|
|
color: #4a9eff;
|
|
font-weight: 300;
|
|
}
|
|
.user-info {
|
|
display: flex;
|
|
gap: 15px;
|
|
align-items: center;
|
|
}
|
|
.username {
|
|
color: #b8c5d6;
|
|
font-size: 16px;
|
|
}
|
|
.logout-btn {
|
|
padding: 10px 20px;
|
|
background: #dc2626;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
text-decoration: none;
|
|
display: inline-block;
|
|
}
|
|
.logout-btn:hover {
|
|
background: #ef4444;
|
|
}
|
|
.request-form {
|
|
background: #1e2835;
|
|
padding: 25px;
|
|
border-radius: 10px;
|
|
margin-bottom: 20px;
|
|
border: 2px solid #2c3e50;
|
|
}
|
|
.form-row {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 15px;
|
|
}
|
|
.form-group {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
label {
|
|
color: #8b9bb0;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
}
|
|
select, input, textarea {
|
|
padding: 12px;
|
|
background: #252f3f;
|
|
border: 2px solid #2c3e50;
|
|
color: #e0e6ed;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
select:focus, input:focus, textarea:focus {
|
|
outline: none;
|
|
border-color: #4a9eff;
|
|
}
|
|
textarea {
|
|
resize: vertical;
|
|
min-height: 100px;
|
|
}
|
|
.headers-input {
|
|
font-size: 13px;
|
|
}
|
|
button {
|
|
padding: 12px 30px;
|
|
background: linear-gradient(135deg, #1e40af 0%, #3b82f6 100%);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 16px;
|
|
font-weight: 500;
|
|
width: 100%;
|
|
}
|
|
button:hover {
|
|
background: linear-gradient(135deg, #2563eb 0%, #60a5fa 100%);
|
|
}
|
|
button:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
.response-section {
|
|
background: #1e2835;
|
|
padding: 25px;
|
|
border-radius: 10px;
|
|
border: 2px solid #2c3e50;
|
|
display: none;
|
|
}
|
|
.response-section.visible {
|
|
display: block;
|
|
}
|
|
.response-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
padding-bottom: 15px;
|
|
border-bottom: 1px solid #2c3e50;
|
|
}
|
|
.status {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
}
|
|
.status.success { color: #22c55e; }
|
|
.status.error { color: #ef4444; }
|
|
.duration {
|
|
color: #8b9bb0;
|
|
font-size: 14px;
|
|
}
|
|
.response-body, .response-headers {
|
|
background: #252f3f;
|
|
padding: 15px;
|
|
border-radius: 6px;
|
|
margin-top: 15px;
|
|
overflow-x: auto;
|
|
}
|
|
.response-body pre, .response-headers pre {
|
|
margin: 0;
|
|
color: #e0e6ed;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
}
|
|
.section-title {
|
|
color: #4a9eff;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
margin-bottom: 10px;
|
|
}
|
|
.error-message {
|
|
background: rgba(239, 68, 68, 0.1);
|
|
border: 1px solid #ef4444;
|
|
color: #ef4444;
|
|
padding: 15px;
|
|
border-radius: 6px;
|
|
margin-top: 15px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>REST API Client</h1>
|
|
<div class="user-info">
|
|
<span class="username">👤 {{.UserID}}</span>
|
|
<a href="/logout" class="logout-btn">Logout</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="request-form">
|
|
<div class="form-row">
|
|
<div class="form-group" style="flex: 0 0 120px;">
|
|
<label>Method</label>
|
|
<select id="method">
|
|
<option>GET</option>
|
|
<option>POST</option>
|
|
<option>PUT</option>
|
|
<option>PATCH</option>
|
|
<option>DELETE</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>URL</label>
|
|
<input type="text" id="url" placeholder="https://api.example.com/endpoint">
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Headers (JSON format)</label>
|
|
<textarea id="headers" class="headers-input" placeholder='{"Content-Type": "application/json", "Authorization": "Bearer token"}'></textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Request Body</label>
|
|
<textarea id="body" placeholder='{"key": "value"}'></textarea>
|
|
</div>
|
|
<button onclick="sendRequest()" id="sendBtn">Send Request</button>
|
|
</div>
|
|
|
|
<div class="response-section" id="responseSection">
|
|
<div class="response-header">
|
|
<span class="status" id="status"></span>
|
|
<span class="duration" id="duration"></span>
|
|
</div>
|
|
<div id="errorMessage" class="error-message" style="display: none;"></div>
|
|
<div id="responseHeaders">
|
|
<div class="section-title">Response Headers</div>
|
|
<div class="response-headers">
|
|
<pre id="headersContent"></pre>
|
|
</div>
|
|
</div>
|
|
<div class="response-body">
|
|
<div class="section-title">Response Body</div>
|
|
<pre id="bodyContent"></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
async function sendRequest() {
|
|
const method = document.getElementById('method').value;
|
|
const url = document.getElementById('url').value;
|
|
const headersText = document.getElementById('headers').value;
|
|
const body = document.getElementById('body').value;
|
|
const sendBtn = document.getElementById('sendBtn');
|
|
const responseSection = document.getElementById('responseSection');
|
|
const errorMessage = document.getElementById('errorMessage');
|
|
|
|
if (!url) {
|
|
alert('Please enter a URL');
|
|
return;
|
|
}
|
|
|
|
let headers = {};
|
|
if (headersText.trim()) {
|
|
try {
|
|
headers = JSON.parse(headersText);
|
|
} catch (e) {
|
|
alert('Invalid JSON in headers');
|
|
return;
|
|
}
|
|
}
|
|
|
|
sendBtn.disabled = true;
|
|
sendBtn.textContent = 'Sending...';
|
|
responseSection.classList.remove('visible');
|
|
errorMessage.style.display = 'none';
|
|
|
|
try {
|
|
const response = await fetch('/api/request', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ method, url, headers, body })
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
responseSection.classList.add('visible');
|
|
|
|
if (result.error) {
|
|
document.getElementById('status').textContent = 'Error';
|
|
document.getElementById('status').className = 'status error';
|
|
errorMessage.textContent = result.error;
|
|
errorMessage.style.display = 'block';
|
|
document.getElementById('responseHeaders').style.display = 'none';
|
|
document.getElementById('bodyContent').textContent = '';
|
|
} else {
|
|
document.getElementById('status').textContent = 'Status: ' + result.status;
|
|
document.getElementById('status').className = result.status < 400 ? 'status success' : 'status error';
|
|
document.getElementById('responseHeaders').style.display = 'block';
|
|
|
|
const formattedHeaders = Object.entries(result.headers)
|
|
.map(([key, value]) => key + ': ' + value)
|
|
.join('\n');
|
|
document.getElementById('headersContent').textContent = formattedHeaders;
|
|
|
|
try {
|
|
const parsed = JSON.parse(result.body);
|
|
document.getElementById('bodyContent').textContent = JSON.stringify(parsed, null, 2);
|
|
} catch {
|
|
document.getElementById('bodyContent').textContent = result.body;
|
|
}
|
|
}
|
|
|
|
document.getElementById('duration').textContent = result.duration + 'ms';
|
|
} catch (error) {
|
|
responseSection.classList.add('visible');
|
|
document.getElementById('status').textContent = 'Request Failed';
|
|
document.getElementById('status').className = 'status error';
|
|
errorMessage.textContent = error.message;
|
|
errorMessage.style.display = 'block';
|
|
document.getElementById('responseHeaders').style.display = 'none';
|
|
} finally {
|
|
sendBtn.disabled = false;
|
|
sendBtn.textContent = 'Send Request';
|
|
}
|
|
}
|
|
|
|
// Allow Enter key in textareas
|
|
document.getElementById('url').addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') sendRequest();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>`
|